├── assets
├── js
│ └── app.js
├── static
│ ├── favicon.ico
│ ├── images
│ │ └── phoenix.png
│ └── robots.txt
└── css
│ └── app.css
├── old-cowboy
├── test
│ └── test_helper.exs
├── .formatter.exs
├── priv
│ ├── static
│ │ └── style.css
│ └── integration
│ │ ├── webring.min.html
│ │ └── webring.html
├── lib
│ ├── webring
│ │ ├── supervisor.ex
│ │ └── handler.ex
│ └── webring.ex
├── .gitignore
├── mix.exs
├── README.md
└── mix.lock
├── .tool-versions
├── priv
├── repo
│ ├── migrations
│ │ └── .formatter.exs
│ └── seeds.exs
├── sites
│ ├── blog-erlang-org.txt
│ ├── underjord.txt
│ ├── stratus3d-com.txt
│ ├── nts.txt
│ ├── hauleth.txt
│ ├── mitchell-hanberg.txt
│ ├── timmo-immo.txt
│ ├── claudio-ortolina.txt
│ ├── rocket-science-ru.txt
│ ├── hostiledeveloper.txt
│ ├── keathley.txt
│ ├── maartenvanvliet.txt
│ ├── patrykbak.txt
│ ├── maqbool.txt
│ ├── arusahni.net
│ ├── christianblavier.txt
│ ├── sayan.txt
│ ├── pthompson.txt
│ ├── bitcrowd-dev.txt
│ ├── angelika-me.txt
│ ├── binarynoggin.com
│ ├── akoutmos.txt
│ └── thegreatcodeadventure.txt
└── gettext
│ ├── en
│ └── LC_MESSAGES
│ │ └── errors.po
│ └── errors.pot
├── test
├── test_helper.exs
├── webring_web
│ └── views
│ │ ├── layout_view_test.exs
│ │ └── error_view_test.exs
├── webring_test.exs
└── support
│ ├── channel_case.ex
│ ├── conn_case.ex
│ └── data_case.ex
├── lib
├── webring_web
│ ├── views
│ │ ├── page_view.ex
│ │ ├── layout_view.ex
│ │ ├── error_view.ex
│ │ └── error_helpers.ex
│ ├── gettext.ex
│ ├── templates
│ │ ├── page
│ │ │ └── index.html.eex
│ │ └── layout
│ │ │ └── app.html.eex
│ ├── channels
│ │ └── user_socket.ex
│ ├── router.ex
│ ├── controllers
│ │ ├── page_controller.ex
│ │ └── ring_controller.ex
│ ├── endpoint.ex
│ └── telemetry.ex
├── webring
│ ├── repo.ex
│ ├── application.ex
│ ├── fair_chance.ex
│ ├── site.ex
│ ├── feed_me
│ │ └── aggregate.ex
│ └── feed_me.ex
├── webring.ex
└── webring_web.ex
├── .formatter.exs
├── config
├── test.exs
├── prod.secret.exs
├── config.exs
├── prod.exs
└── dev.exs
├── .gitignore
├── Dockerfile
├── mix.exs
├── README.md
└── mix.lock
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | import "../css/app.css"
2 |
--------------------------------------------------------------------------------
/old-cowboy/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | elixir 1.14.3-otp-25
2 | erlang 25.3.1
3 |
--------------------------------------------------------------------------------
/priv/repo/migrations/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto_sql],
3 | inputs: ["*.exs"]
4 | ]
5 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 | # Ecto.Adapters.SQL.Sandbox.mode(Webring.Repo, :manual)
3 |
--------------------------------------------------------------------------------
/assets/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lawik/beambloggers/HEAD/assets/static/favicon.ico
--------------------------------------------------------------------------------
/lib/webring_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.PageView do
2 | use WebringWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/webring_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.LayoutView do
2 | use WebringWeb, :view
3 | end
4 |
--------------------------------------------------------------------------------
/assets/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lawik/beambloggers/HEAD/assets/static/images/phoenix.png
--------------------------------------------------------------------------------
/old-cowboy/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4 | ]
5 |
--------------------------------------------------------------------------------
/lib/webring/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.Repo do
2 | use Ecto.Repo,
3 | otp_app: :webring,
4 | adapter: Ecto.Adapters.Postgres
5 | end
6 |
--------------------------------------------------------------------------------
/priv/sites/blog-erlang-org.txt:
--------------------------------------------------------------------------------
1 | https://blog.erlang.org
2 |
3 | Erlang/OTP technical blog
4 |
5 | The official blog of the Erlang/OTP team at Ericsson.
6 |
--------------------------------------------------------------------------------
/priv/sites/underjord.txt:
--------------------------------------------------------------------------------
1 | https://underjord.io
2 |
3 | Underjord
4 |
5 | Creator of the Beam Blogger Webring. Elixir consultant with *too much enthusiasm*.
6 |
--------------------------------------------------------------------------------
/priv/sites/stratus3d-com.txt:
--------------------------------------------------------------------------------
1 | https://stratus3d.com
2 |
3 | Trevor Brown
4 |
5 | Software engineer building things with Erlang, Elixir, Ruby, Lua, and JavaScript.
6 |
--------------------------------------------------------------------------------
/priv/sites/nts.txt:
--------------------------------------------------------------------------------
1 | https://nts.strzibny.name/
2 |
3 | Josef Strzibny
4 |
5 | Josef is a full-stack software engineer interested in all things Elixir, Ruby, and Linux.
6 |
--------------------------------------------------------------------------------
/priv/sites/hauleth.txt:
--------------------------------------------------------------------------------
1 | https://hauleth.dev
2 |
3 | Łukasz Jan Niemier
4 |
5 | I have written some stuff in Elixir and Erlang. I also try to write about BEAM from time to time.
6 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto, :phoenix],
3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
4 | subdirectories: ["priv/*/migrations"]
5 | ]
6 |
--------------------------------------------------------------------------------
/priv/sites/mitchell-hanberg.txt:
--------------------------------------------------------------------------------
1 | https://www.mitchellhanberg.com
2 |
3 | Mitchell Hanberg
4 |
5 | Mitch is a full stack software engineer, focusing on building maintainable systems with Elixir.
6 |
--------------------------------------------------------------------------------
/priv/sites/timmo-immo.txt:
--------------------------------------------------------------------------------
1 | https://timmo.immo
2 |
3 | Timmo Verlaan
4 |
5 | Timmo Verlaan is a lead and software engineer based in Amsterdam. He enjoys Elixir a lot and talks/blogs about it.
6 |
--------------------------------------------------------------------------------
/priv/sites/claudio-ortolina.txt:
--------------------------------------------------------------------------------
1 | https://claudio-ortolina.org/posts/index.xml
2 |
3 | Claudio Ortolina
4 |
5 | Claudio is a software engineer and speaker, blogging about Elixir/OTP and other things.
6 |
--------------------------------------------------------------------------------
/priv/sites/rocket-science-ru.txt:
--------------------------------------------------------------------------------
1 | https://rocket-science.ru
2 |
3 | Aleksei Matiushkin
4 |
5 | Born on October 0th. More functional than object-oriented. Maintainer of (https://hex.pm/users/mudasobwa).
6 |
--------------------------------------------------------------------------------
/priv/sites/hostiledeveloper.txt:
--------------------------------------------------------------------------------
1 | https://hostiledeveloper.com
2 |
3 | Steven Nunez
4 |
5 | Elixir code slinger at Flatiron School engineering. Poisoning the minds of the youth with functional programming.
6 |
--------------------------------------------------------------------------------
/priv/sites/keathley.txt:
--------------------------------------------------------------------------------
1 | https://keathley.io
2 |
3 | Chris Keathley
4 |
5 | Chris Keathley is a software engineer building systems with Elixir and Erlang. He writes about software design, open source, and cool Elixir ideas.
6 |
--------------------------------------------------------------------------------
/priv/sites/maartenvanvliet.txt:
--------------------------------------------------------------------------------
1 | https://maartenvanvliet.nl
2 |
3 | Maarten van Vliet
4 |
5 | Maarten van Vliet is a software engineer specializing in backend development. Builds digital products with Elixir, TypeScript and Ruby.
6 |
--------------------------------------------------------------------------------
/priv/sites/patrykbak.txt:
--------------------------------------------------------------------------------
1 | https://patrykbak.com
2 |
3 | Patryk Bak
4 |
5 | A software developer specializing in backend development. Builds digital products, which scale a business and facilitate people's lives, with Elixir.
6 |
--------------------------------------------------------------------------------
/priv/sites/maqbool.txt:
--------------------------------------------------------------------------------
1 | https://www.maqbool.net
2 |
3 | Mohammad Maqbool Alam
4 |
5 | Maqbool is a Software Developer 👨🏽💻 Interested in Distributed systems, Kubernetes, Functional programming, elixirlang, elmlang, Haskell, and OSS.
--------------------------------------------------------------------------------
/priv/sites/arusahni.net:
--------------------------------------------------------------------------------
1 | https://arusahni.net
2 |
3 | Aru Sahni
4 |
5 | Aru is a DC-area developer (and Vim-thusiast) who enjoys playing on all parts of the stack. He has recently picked up Elixir, and is blogging about his experiences.
6 |
--------------------------------------------------------------------------------
/assets/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/priv/sites/christianblavier.txt:
--------------------------------------------------------------------------------
1 | https://www.christianblavier.com/
2 |
3 | Christian Blavier
4 |
5 | Currently VP Technology at PhenixDigital, I'm a passionate Elixir, Ruby & JS programmer. Former founder and CTO at sharypic.com, folyo.me.
6 |
--------------------------------------------------------------------------------
/priv/sites/sayan.txt:
--------------------------------------------------------------------------------
1 | https://sayan.xyz
2 |
3 | Sayan Chakraborty
4 |
5 | Sayan is a Senior Software Engineer and incoming Brown University graduate student in CS. He loves writing about Elixir/Erlang, distributed systems and engineering experiences.
--------------------------------------------------------------------------------
/priv/sites/pthompson.txt:
--------------------------------------------------------------------------------
1 | http://blog.pthompson.org
2 |
3 | Patrick Thompson
4 |
5 | Indie developer and founder at Inkstone Software. Writes occasionally on the PETAL stack, which is comprised of Phoenix, Elixir, Tailwind CSS, AlpineJS, and LiveView.
6 |
--------------------------------------------------------------------------------
/priv/sites/bitcrowd-dev.txt:
--------------------------------------------------------------------------------
1 | https://bitcrowd.dev/
2 |
3 | bitcrowd
4 |
5 | bitcrowd is a software agency from Berlin, Germany. They work with Elixir and the Beam and frequently blog about it and other web development or software engineering related topics.
6 |
--------------------------------------------------------------------------------
/priv/sites/angelika-me.txt:
--------------------------------------------------------------------------------
1 | https://angelika.me/blog
2 |
3 | Angelika Tyborska
4 |
5 | Angelika is a web developer based in Berlin, Germany. She blogs about Elixir and web development, maintains the Elixir track on Exrcism.io, and makes silly things with CSS.
6 |
--------------------------------------------------------------------------------
/priv/sites/binarynoggin.com:
--------------------------------------------------------------------------------
1 | https://binarynoggin.com
2 |
3 | Binary Noggin
4 |
5 | Binary Noggin is a group of software engineers and architects who act as an extensionof your team, helping you succeed by developing tailored, flexible solutions in Elixir, Ruby and more.
6 |
--------------------------------------------------------------------------------
/lib/webring.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring do
2 | @moduledoc """
3 | Webring keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/test/webring_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.LayoutViewTest do
2 | use WebringWeb.ConnCase, async: true
3 |
4 | # When testing helpers, you may want to import Phoenix.HTML and
5 | # use functions such as safe_to_string() to convert the helper
6 | # result into an HTML string.
7 | # import Phoenix.HTML
8 | end
9 |
--------------------------------------------------------------------------------
/priv/sites/akoutmos.txt:
--------------------------------------------------------------------------------
1 | https://akoutmos.com
2 |
3 | Alex Koutmos
4 |
5 | Alex is a Senior Software Engineer who writes backends in Elixir, frontends in VueJS and deploys his apps using Kubernetes. He is also a panelist on the Elixir Mix podcast (https://devchat.tv/podcasts/elixir-mix/) and maintains several Open Source packages on Hex (https://hex.pm/users/akoutmos).
6 |
--------------------------------------------------------------------------------
/priv/sites/thegreatcodeadventure.txt:
--------------------------------------------------------------------------------
1 | https://www.thegreatcodeadventure.com/
2 |
3 | Sophie DeBenedetto
4 |
5 | Sophie is engineer currently writing code for GitHub, as well as a maintainer of Elixir School https://elixirschool.com/en/ and a host of the Elixir Mix podcast https://devchat.tv/podcasts/elixir-mix/ View [her cute dog](https://www.thegreatcodeadventure.com/assets/images/moebi.png?v=c0d041046c)
6 |
--------------------------------------------------------------------------------
/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # Webring.Repo.insert!(%Webring.SomeSchema{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/old-cowboy/priv/static/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap');
2 |
3 | body {
4 | background-color: rgb(162,0,62); /* Erlang Red */
5 | color: white;
6 | font-family: 'Titillium Web', helvetica, arial, 'sans-serif';
7 | padding: 16px;
8 | }
9 |
10 | h1, p, .site {
11 | max-width: 800px;
12 | margin-left: auto;
13 | margin-right: auto;
14 | }
15 |
16 | a,
17 | a:link,
18 | a:active,
19 | a:visited {
20 | color: white;
21 | }
22 |
--------------------------------------------------------------------------------
/test/webring_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.ErrorViewTest do
2 | use WebringWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | # import Phoenix.View
6 |
7 | # test "renders 404.html" do
8 | # assert render_to_string(WebringWeb.ErrorView, "404.html", []) == "Not Found"
9 | # end
10 |
11 | # test "renders 500.html" do
12 | # assert render_to_string(WebringWeb.ErrorView, "500.html", []) == "Internal Server Error"
13 | # end
14 | end
15 |
--------------------------------------------------------------------------------
/old-cowboy/lib/webring/supervisor.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.Supervisor do
2 | use Supervisor
3 |
4 | def start_link(opts) do
5 | {args, opts} = Keyword.pop!(opts, :cowboy_args)
6 | Supervisor.start_link(__MODULE__, args, opts)
7 | end
8 |
9 | @impl true
10 | def init(args) do
11 | children = [
12 | Webring.FairChance,
13 | {Finch, name: :default},
14 | Webring.FeedMe,
15 | %{id: :cowboy, start: {:cowboy, :start_clear, args}}
16 | ]
17 |
18 | Supervisor.init(children, strategy: :one_for_one)
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/webring_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.ErrorView do
2 | use WebringWeb, :view
3 |
4 | # If you want to customize a particular status code
5 | # for a certain format, you may uncomment below.
6 | # def render("500.html", _assigns) do
7 | # "Internal Server Error"
8 | # end
9 |
10 | # By default, Phoenix returns the status message from
11 | # the template name. For example, "404.html" becomes
12 | # "Not Found".
13 | def template_not_found(template, _assigns) do
14 | Phoenix.Controller.status_message_from_template(template)
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/assets/css/app.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Titillium+Web&display=swap");
2 |
3 | body {
4 | background-color: rgb(162, 0, 62); /* Erlang Red */
5 | color: white;
6 | font-family: "Titillium Web", helvetica, arial, "sans-serif";
7 | padding: 16px;
8 | }
9 |
10 | h1,
11 | p,
12 | .site,
13 | .latest {
14 | max-width: 800px;
15 | margin-left: auto;
16 | margin-right: auto;
17 | }
18 |
19 | a,
20 | a:link,
21 | a:active,
22 | a:visited {
23 | color: white;
24 | text-decoration: underline;
25 | }
26 |
27 | ul {
28 | list-style-type: square;
29 | }
30 |
--------------------------------------------------------------------------------
/test/webring_test.exs:
--------------------------------------------------------------------------------
1 | defmodule WebringTest do
2 | use ExUnit.Case
3 | doctest Webring
4 |
5 | test "fair chance is fair" do
6 | assert spin() == :fair
7 | end
8 |
9 | defp spin do
10 | site = Webring.FairChance.rotate()
11 | spin([], site)
12 | end
13 |
14 | defp spin(seen, start) do
15 | if start in seen do
16 | if length(Enum.uniq(seen)) == length(seen) do
17 | :fair
18 | else
19 | :unfair
20 | end
21 | else
22 | site = Webring.FairChance.rotate()
23 | spin([site | seen], start)
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/old-cowboy/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | webring-*.tar
24 |
25 | priv/generated/index.html
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | #
5 | # The MIX_TEST_PARTITION environment variable can be used
6 | # to provide built-in test partitioning in CI environment.
7 | # Run `mix help test` for more information.
8 | config :webring, Webring.Repo,
9 | username: "postgres",
10 | password: "postgres",
11 | database: "webring_test#{System.get_env("MIX_TEST_PARTITION")}",
12 | hostname: "localhost",
13 | pool: Ecto.Adapters.SQL.Sandbox
14 |
15 | # We don't run a server during test. If one is required,
16 | # you can enable the server option below.
17 | config :webring, WebringWeb.Endpoint,
18 | http: [port: 4002],
19 | server: false
20 |
21 | # Print only warnings and errors during test
22 | config :logger, level: :warn
23 |
--------------------------------------------------------------------------------
/old-cowboy/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Webring.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :webring,
7 | version: "0.1.0",
8 | elixir: "~> 1.10",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps()
11 | ]
12 | end
13 |
14 | # Run "mix help compile.app" to learn about applications.
15 | def application do
16 | [
17 | mod: {Webring, []},
18 | extra_applications: [:cowboy, :ranch, :logger]
19 | ]
20 | end
21 |
22 | # Run "mix help deps" to learn about dependencies.
23 | defp deps do
24 | [
25 | {:cowboy, "~> 2.8"},
26 | {:earmark, "~> 1.4"},
27 | {:fast_rss, "~> 0.3.4"},
28 | {:finch, "~> 0.5"},
29 | {:floki, "~> 0.29"}
30 | ]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/webring_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import WebringWeb.Gettext
9 |
10 | # Simple translation
11 | gettext("Here is the string to translate")
12 |
13 | # Plural translation
14 | ngettext("Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3)
17 |
18 | # Domain-based translation
19 | dgettext("errors", "Here is the error message to translate")
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :webring
24 | end
25 |
--------------------------------------------------------------------------------
/old-cowboy/lib/webring/handler.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.Handler do
2 | @default_headers %{"content-type" => "text/html"}
3 | def init(req, state) do
4 | handle(req, state)
5 | end
6 |
7 | def handle(%{path: "/shuffle"} = request, state) do
8 | {_, url} = Webring.FairChance.rotate()
9 |
10 | response =
11 | :cowboy_req.reply(
12 | 302,
13 | Map.put(@default_headers, "location", url),
14 | "",
15 | request
16 | )
17 |
18 | {:ok, response, state}
19 | end
20 |
21 | def handle(request, state) do
22 | response =
23 | :cowboy_req.reply(
24 | 200,
25 | @default_headers,
26 | "
Beam Bloggers Webring ",
27 | request
28 | )
29 |
30 | {:ok, response, state}
31 | end
32 |
33 | def terminate(_reason, _request, _state) do
34 | :ok
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/webring_web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 | Latest from the feeds
3 |
4 | <%= for %{item: item, site: site} <- @latest do %>
5 | <%= item[:title] %> by <%= site.title %> - <%= item[:datetime] |> Timex.format!("{ISOdate}") %>
6 | <% end %>
7 |
8 |
9 |
10 |
11 | <%= for site <- @sites do %>
12 |
13 | <%= site.title %>
14 | <%= site.url %>
15 | <%= {:safe, site.fancy_body} %>
16 | <%= if @feeds[site.hash] && Enum.count(@feeds[site.hash][:items]) > 0 do %>
17 |
18 | <%= for item <- Enum.take(@feeds[site.hash][:items], 3) do %>
19 | <%= item[:title] %> - <%= item[:datetime] |> Timex.format!("{ISOdate}") %>
20 | <% end %>
21 |
22 | <% end %>
23 |
24 | <% end %>
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | webring-*.tar
24 |
25 | # If NPM crashes, it generates a log, let's ignore it too.
26 | npm-debug.log
27 |
28 | # The directory NPM downloads your dependencies sources to.
29 | /assets/node_modules/
30 |
31 | # Since we are building assets from assets/,
32 | # we ignore priv/static. You may want to comment
33 | # this depending on your deployment strategy.
34 | /priv/static/
35 |
--------------------------------------------------------------------------------
/config/prod.secret.exs:
--------------------------------------------------------------------------------
1 | # In this file, we load production configuration and secrets
2 | # from environment variables. You can also hardcode secrets,
3 | # although such is generally not recommended and you have to
4 | # remember to add this file to your .gitignore.
5 | use Mix.Config
6 |
7 | secret_key_base =
8 | System.get_env("SECRET_KEY_BASE") ||
9 | raise """
10 | environment variable SECRET_KEY_BASE is missing.
11 | You can generate one by calling: mix phx.gen.secret
12 | """
13 |
14 | config :webring, WebringWeb.Endpoint,
15 | http: [
16 | port: String.to_integer(System.get_env("PORT") || "20000"),
17 | transport_options: [socket_opts: [:inet6]]
18 | ],
19 | secret_key_base: secret_key_base
20 |
21 | # ## Using releases (Elixir v1.9+)
22 | #
23 | # If you are doing OTP releases, you need to instruct Phoenix
24 | # to start each relevant endpoint:
25 | #
26 | config :webring, WebringWeb.Endpoint, server: true
27 | #
28 | # Then you can assemble a release by calling `mix release`.
29 | # See `mix help release` for more information.
30 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 |
7 | # General application configuration
8 | use Mix.Config
9 |
10 | config :webring,
11 | ecto_repos: [Webring.Repo]
12 |
13 | # Configures the endpoint
14 | config :webring, WebringWeb.Endpoint,
15 | url: [host: "localhost"],
16 | secret_key_base: "smPNz87iH9Aq5VcNhVeP378ZYYJxh2VxZORWCyzq+jDK1Vn1KJ5D0ycSNEA+ZNuQ",
17 | render_errors: [view: WebringWeb.ErrorView, accepts: ~w(html json), layout: false],
18 | pubsub_server: Webring.PubSub,
19 | live_view: [signing_salt: "iewbhP82"]
20 |
21 | # Configures Elixir's Logger
22 | config :logger, :console,
23 | format: "$time $metadata[$level] $message\n",
24 | metadata: [:request_id]
25 |
26 | # Use Jason for JSON parsing in Phoenix
27 | config :phoenix, :json_library, Jason
28 |
29 | # Import environment specific config. This must remain at the bottom
30 | # of this file so it overrides the configuration defined above.
31 | import_config "#{Mix.env()}.exs"
32 |
--------------------------------------------------------------------------------
/old-cowboy/priv/integration/webring.min.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/webring_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", WebringWeb.RoomChannel
6 |
7 | # Socket params are passed from the client and can
8 | # be used to verify and authenticate a user. After
9 | # verification, you can put default assigns into
10 | # the socket that will be set for all channels, ie
11 | #
12 | # {:ok, assign(socket, :user_id, verified_user_id)}
13 | #
14 | # To deny connection, return `:error`.
15 | #
16 | # See `Phoenix.Token` documentation for examples in
17 | # performing token verification on connect.
18 | @impl true
19 | def connect(_params, socket, _connect_info) do
20 | {:ok, socket}
21 | end
22 |
23 | # Socket id's are topics that allow you to identify all sockets for a given user:
24 | #
25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
26 | #
27 | # Would allow you to broadcast a "disconnect" event and terminate
28 | # all active sockets and channels for a given user:
29 | #
30 | # WebringWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
31 | #
32 | # Returning `nil` makes this socket anonymous.
33 | @impl true
34 | def id(_socket), do: nil
35 | end
36 |
--------------------------------------------------------------------------------
/lib/webring/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | def start(_type, _args) do
9 | children = [
10 | # Start the Ecto repository
11 | # Webring.Repo,
12 | # Start the Telemetry supervisor
13 | WebringWeb.Telemetry,
14 | # Start the PubSub system
15 | {Phoenix.PubSub, name: Webring.PubSub},
16 | # Start the Endpoint (http/https)
17 | WebringWeb.Endpoint,
18 | # Webring stuff
19 | {Finch, name: :default},
20 | Webring.FairChance,
21 | Webring.FeedMe
22 | # Start a worker by calling: Webring.Worker.start_link(arg)
23 | # {Webring.Worker, arg}
24 | ]
25 |
26 | # See https://hexdocs.pm/elixir/Supervisor.html
27 | # for other strategies and supported options
28 | opts = [strategy: :one_for_one, name: Webring.Supervisor]
29 | Supervisor.start_link(children, opts)
30 | end
31 |
32 | # Tell Phoenix to update the endpoint configuration
33 | # whenever the application is updated.
34 | def config_change(changed, _new, removed) do
35 | WebringWeb.Endpoint.config_change(changed, removed)
36 | :ok
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use WebringWeb.ChannelCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # Import conveniences for testing with channels
23 | import Phoenix.ChannelTest
24 | import WebringWeb.ChannelCase
25 |
26 | # The default endpoint for testing
27 | @endpoint WebringWeb.Endpoint
28 | end
29 | end
30 |
31 | setup tags do
32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Webring.Repo)
33 |
34 | unless tags[:async] do
35 | Ecto.Adapters.SQL.Sandbox.mode(Webring.Repo, {:shared, self()})
36 | end
37 |
38 | :ok
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/webring/fair_chance.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.FairChance do
2 | @moduledoc false
3 | use GenServer
4 |
5 | def start_link(_) do
6 | GenServer.start_link(Webring.FairChance, nil, name: Webring.FairChance)
7 | end
8 |
9 | @impl true
10 | def init(nil) do
11 | sorted_sites = build_site_rotation()
12 | {:ok, {sorted_sites, [], sorted_sites}}
13 | end
14 |
15 | @impl true
16 | def handle_call(:rotate, _from, state) do
17 | {site, state} = rotate_next(state)
18 | {:reply, site, state}
19 | end
20 |
21 | @impl true
22 | def handle_call(:list_sites, _from, {_, _, site_list} = state) do
23 | {:reply, site_list, state}
24 | end
25 |
26 | def rotate do
27 | GenServer.call(Webring.FairChance, :rotate)
28 | end
29 |
30 | def list_sites do
31 | GenServer.call(Webring.FairChance, :list_sites)
32 | end
33 |
34 | defp build_site_rotation do
35 | Webring.Site.list_sites()
36 | |> Enum.sort()
37 | end
38 |
39 | defp rotate_next(state) do
40 | case state do
41 | {[], sites, list} ->
42 | sites = Enum.reverse(sites)
43 | [site | sites] = sites
44 | {site, {sites, [site], list}}
45 |
46 | {[site | sites], rotated, list} ->
47 | {site, {sites, [site | rotated], list}}
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM elixir:1.10.4 AS build
2 |
3 | # install build dependencies
4 | RUN curl -sL https://deb.nodesource.com/setup_lts.x | bash -
5 | RUN apt-get update && apt-get install -y build-essential nodejs
6 |
7 | # prepare build dir
8 | WORKDIR /app
9 |
10 | # install hex + rebar
11 | RUN mix local.hex --force && \
12 | mix local.rebar --force
13 |
14 | # set build ENV
15 | ENV MIX_ENV=prod SECRET_KEY_BASE="gMG5ncoLVAnCKHWSHMVKmvU0Cgju+ZqLbvRYm8OD3H9e9rgnR1rvh2oYyywbdncv"
16 | # prep mix
17 | COPY mix.exs mix.lock ./
18 | COPY config config
19 | # install mix dependencies
20 | RUN mix do deps.get, deps.compile
21 |
22 | # build assets
23 | COPY assets/package.json assets/package-lock.json ./assets/
24 | RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error
25 |
26 | COPY priv priv
27 | COPY assets assets
28 | RUN npm run --prefix ./assets deploy
29 | RUN mix phx.digest
30 |
31 | # compile and build release
32 | COPY lib lib
33 | # uncomment COPY if rel/ exists
34 | # COPY rel rel
35 | RUN mix do compile, release
36 |
37 | # prepare release image
38 | FROM debian:buster-slim AS app
39 | RUN apt-get update && apt-get install -y openssl libncursesw6
40 |
41 | WORKDIR /app
42 |
43 | #COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/webring ./
44 | COPY --from=build /app/_build/prod/rel/webring ./
45 |
46 | ENV HOME=/app
47 |
48 | CMD ["bin/webring", "start"]
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common data structures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | we enable the SQL sandbox, so changes done to the database
12 | are reverted at the end of every test. If you are using
13 | PostgreSQL, you can even run database tests asynchronously
14 | by setting `use WebringWeb.ConnCase, async: true`, although
15 | this option is not recommended for other databases.
16 | """
17 |
18 | use ExUnit.CaseTemplate
19 |
20 | using do
21 | quote do
22 | # Import conveniences for testing with connections
23 | import Plug.Conn
24 | import Phoenix.ConnTest
25 | import WebringWeb.ConnCase
26 |
27 | alias WebringWeb.Router.Helpers, as: Routes
28 |
29 | # The default endpoint for testing
30 | @endpoint WebringWeb.Endpoint
31 | end
32 | end
33 |
34 | setup tags do
35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Webring.Repo)
36 |
37 | unless tags[:async] do
38 | Ecto.Adapters.SQL.Sandbox.mode(Webring.Repo, {:shared, self()})
39 | end
40 |
41 | {:ok, conn: Phoenix.ConnTest.build_conn()}
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/webring_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.Router do
2 | use WebringWeb, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :api do
13 | plug :accepts, ["json"]
14 | end
15 |
16 | scope "/", WebringWeb do
17 | pipe_through :browser
18 |
19 | get "/", PageController, :index
20 | get "/feed", PageController, :rss
21 |
22 | get "/random", RingController, :random
23 | get "/prev", RingController, :prev
24 | get "/next", RingController, :next
25 | end
26 |
27 | # Other scopes may use custom stacks.
28 | # scope "/api", WebringWeb do
29 | # pipe_through :api
30 | # end
31 |
32 | # Enables LiveDashboard only for development
33 | #
34 | # If you want to use the LiveDashboard in production, you should put
35 | # it behind authentication and allow only admins to access it.
36 | # If your application does not have an admins-only section yet,
37 | # you can use Plug.BasicAuth to set up some basic authentication
38 | # as long as you are also using SSL (which you should anyway).
39 | if Mix.env() in [:dev, :test] do
40 | import Phoenix.LiveDashboard.Router
41 |
42 | scope "/" do
43 | pipe_through :browser
44 | live_dashboard "/dashboard", metrics: WebringWeb.Telemetry
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/webring_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Beam Bloggers Webring
8 | "/>
9 |
10 |
11 |
12 |
13 |
14 | Beam Bloggers Webring
15 | If you have a blog that regularly covers Elixir, Erlang, the BEAM or any related topics do
16 | join the webring by sending us a PR. If
17 | you want to follow our intrepid bloggers and discover new ones as they join you can
18 | subscribe to our RSS feed
19 | which aggregates all of the below sites that offer RSS. You can always enjoy a random blog .
20 | <%= get_flash(@conn, :info) %>
21 | <%= get_flash(@conn, :error) %>
22 | <%= @inner_content %>
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/webring_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.PageController do
2 | use WebringWeb, :controller
3 |
4 | @latest_limit 12
5 |
6 | def index(conn, _params) do
7 | sites = Webring.Site.list_sites()
8 | feeds = Webring.FeedMe.list_feeds()
9 |
10 | latest =
11 | feeds
12 | |> Enum.filter(fn {_, feed} ->
13 | Enum.count(feed.items) > 0
14 | end)
15 | |> Enum.map(fn {site_hash, %{items: [item | _]}} ->
16 | site =
17 | Enum.find(sites, fn site ->
18 | site.hash == site_hash
19 | end)
20 |
21 | %{item: item, site: site}
22 | end)
23 | |> Enum.sort_by(
24 | fn entry ->
25 | entry.item[:iso_datetime]
26 | end,
27 | :desc
28 | )
29 | |> Enum.take(@latest_limit)
30 |
31 | sites =
32 | sites
33 | |> Enum.sort_by(
34 | fn site ->
35 | feed = feeds[site.hash]
36 |
37 | if not is_nil(feed) and feed.items != [] do
38 | [latest | _] = feed.items
39 | latest[:iso_datetime]
40 | else
41 | # bump to bottom sorting if no RSS or items
42 | "2001" <> site.hash
43 | end
44 | end,
45 | :desc
46 | )
47 |
48 | render(conn, "index.html", %{sites: sites, feeds: feeds, latest: latest})
49 | end
50 |
51 | def rss(conn, _params) do
52 | feed_xml = Webring.FeedMe.get_rss()
53 |
54 | conn
55 | |> put_resp_content_type("text/xml")
56 | |> send_resp(200, feed_xml)
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/webring_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn error ->
13 | content_tag(:span, translate_error(error),
14 | class: "invalid-feedback",
15 | phx_feedback_for: input_id(form, field)
16 | )
17 | end)
18 | end
19 |
20 | @doc """
21 | Translates an error message using gettext.
22 | """
23 | def translate_error({msg, opts}) do
24 | # When using gettext, we typically pass the strings we want
25 | # to translate as a static argument:
26 | #
27 | # # Translate "is invalid" in the "errors" domain
28 | # dgettext("errors", "is invalid")
29 | #
30 | # # Translate the number of files with plural rules
31 | # dngettext("errors", "1 file", "%{count} files", count)
32 | #
33 | # Because the error messages we show in our forms and APIs
34 | # are defined inside Ecto, we need to translate them dynamically.
35 | # This requires us to call the Gettext module passing our gettext
36 | # backend as first argument.
37 | #
38 | # Note we use the "errors" domain, which means translations
39 | # should be written to the errors.po file. The :count option is
40 | # set by Ecto and indicates we should also apply plural rules.
41 | if count = opts[:count] do
42 | Gettext.dngettext(WebringWeb.Gettext, "errors", msg, msg, count, opts)
43 | else
44 | Gettext.dgettext(WebringWeb.Gettext, "errors", msg, opts)
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/support/data_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.DataCase do
2 | @moduledoc """
3 | This module defines the setup for tests requiring
4 | access to the application's data layer.
5 |
6 | You may define functions here to be used as helpers in
7 | your tests.
8 |
9 | Finally, if the test case interacts with the database,
10 | we enable the SQL sandbox, so changes done to the database
11 | are reverted at the end of every test. If you are using
12 | PostgreSQL, you can even run database tests asynchronously
13 | by setting `use Webring.DataCase, async: true`, although
14 | this option is not recommended for other databases.
15 | """
16 |
17 | use ExUnit.CaseTemplate
18 |
19 | using do
20 | quote do
21 | alias Webring.Repo
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | import Webring.DataCase
27 | end
28 | end
29 |
30 | setup tags do
31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Webring.Repo)
32 |
33 | unless tags[:async] do
34 | Ecto.Adapters.SQL.Sandbox.mode(Webring.Repo, {:shared, self()})
35 | end
36 |
37 | :ok
38 | end
39 |
40 | @doc """
41 | A helper that transforms changeset errors into a map of messages.
42 |
43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"})
44 | assert "password is too short" in errors_on(changeset).password
45 | assert %{password: ["password is too short"]} = errors_on(changeset)
46 |
47 | """
48 | def errors_on(changeset) do
49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
50 | Regex.replace(~r"%{(\w+)}", message, fn _, key ->
51 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
52 | end)
53 | end)
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/webring_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :webring
3 |
4 | # The session will be stored in the cookie and signed,
5 | # this means its contents can be read but not tampered with.
6 | # Set :encryption_salt if you would also like to encrypt it.
7 | @session_options [
8 | store: :cookie,
9 | key: "_webring_key",
10 | signing_salt: "MKUuxigf"
11 | ]
12 |
13 | socket "/socket", WebringWeb.UserSocket,
14 | websocket: true,
15 | longpoll: false
16 |
17 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
18 |
19 | # Serve at "/" the static files from "priv/static" directory.
20 | #
21 | # You should set gzip to true if you are running phx.digest
22 | # when deploying your static files in production.
23 | plug Plug.Static,
24 | at: "/static",
25 | from: :webring,
26 | gzip: false,
27 | only: ~w(assets)
28 |
29 | # Code reloading can be explicitly enabled under the
30 | # :code_reloader configuration of your endpoint.
31 | if code_reloading? do
32 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
33 | plug Phoenix.LiveReloader
34 | plug Phoenix.CodeReloader
35 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :webring
36 | end
37 |
38 | plug Phoenix.LiveDashboard.RequestLogger,
39 | param_key: "request_logger",
40 | cookie_key: "request_logger"
41 |
42 | plug Plug.RequestId
43 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
44 |
45 | plug Plug.Parsers,
46 | parsers: [:urlencoded, :multipart, :json],
47 | pass: ["*/*"],
48 | json_decoder: Phoenix.json_library()
49 |
50 | plug Plug.MethodOverride
51 | plug Plug.Head
52 | plug Plug.Session, @session_options
53 | plug WebringWeb.Router
54 | end
55 |
--------------------------------------------------------------------------------
/old-cowboy/README.md:
--------------------------------------------------------------------------------
1 | # The BEAM Blogger Webring
2 |
3 | ## What is a Webring
4 |
5 | Webrings are an old school web thing that is having a small resurgence these days. The idea is that a bunch of sites with related subject matter join a (circular) list and they all include a small element to send traffic to the other parts of the Webring. Recently famed is the [Weird Wide Webring](https://weirdwidewebring.net/) that gathers interesting, odd and quirky sites. We'll be the next semi-medium-sized thing.
6 |
7 | ## How to join (or leave)
8 |
9 | Check out `priv/sites` and add your site following the way `underjord.txt` is laid out in a new file. Stick to a single paragraph of text. Submit it as a PR and we will take a look.
10 |
11 | Criteria:
12 | - Run your own site (not Medium, dev.to), if this seems bad, let us know in the issues
13 | - Your blog covers BEAM languages (Elixir, Erlang, Gleam, LFE and any others) or something closely related
14 | - There is no requirement for how often you post
15 |
16 | When you've been added we strongly recommend you integrate the shuffler to give back some idle traffic to the Webring. It doesn't do anything harmful. It is just a static piece of code with a couple of outbound links.
17 |
18 | Somewhere below your content is recommended. Feel free to adjust the styling to fit your site but preferrably keep all the elements.
19 |
20 | ## How to integrate the shuffler
21 |
22 | In `integration` you'll find the `webring.min.html` and `webring.html` which is the markup you need to integrate in your site. Just pick one, the .min is less readable but denser.
23 |
24 | Once published you will be able to pull the current integration by just sending a GET request to /integration.
25 |
26 | ## How to get rid of us
27 |
28 | Just send a PR to have your site removed and remove the shuffler from your site :)
--------------------------------------------------------------------------------
/lib/webring_web/controllers/ring_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.RingController do
2 | use WebringWeb, :controller
3 | require Logger
4 |
5 | def random(conn, _params) do
6 | random_url =
7 | Webring.Site.list_sites()
8 | |> Enum.random()
9 | |> ring_url()
10 |
11 | redirect(conn, external: random_url)
12 | end
13 |
14 | def next(conn, params), do: relative(conn, params, +1)
15 | def prev(conn, params), do: relative(conn, params, -1)
16 |
17 | defp relative(conn, params, rel) do
18 | referer =
19 | case Map.get(params, "referer") do
20 | nil -> hd(get_req_header(conn, "referer"))
21 | ref -> "https://#{ref}"
22 | end
23 |
24 | # we trust that the sites are ordered according to ""logic"" in Webring.Site.list_sites/0 :-)
25 | url =
26 | Webring.Site.list_sites()
27 | |> find_relative(referer, rel)
28 | |> ring_url()
29 |
30 | redirect(conn, external: url)
31 | end
32 |
33 | defp ring_url(site) do
34 | site
35 | |> Map.get(:url)
36 | |> URI.parse()
37 | |> URI.merge("?ref=#{WebringWeb.Endpoint.host()}")
38 | |> URI.to_string()
39 | end
40 |
41 | defp find_relative(sites, referer, rel) do
42 | with index when is_integer(index) <- Enum.find_index(sites, &host_match?(&1.url, referer)),
43 | %Webring.Site{} = site <- Enum.at(sites, index + rel, :out_of_bounds) do
44 | site
45 | else
46 | :out_of_bounds ->
47 | # since we're circular we should grab the first
48 | hd(sites)
49 |
50 | nil ->
51 | Logger.info("Webring, ringmember not found: #{referer}")
52 | Enum.random(sites)
53 | end
54 | end
55 |
56 | defp host_match?(uri1, uri2) do
57 | uri1 = URI.parse(uri1)
58 | uri2 = URI.parse(uri2)
59 | uri1.host == uri2.host
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/old-cowboy/priv/integration/webring.html:
--------------------------------------------------------------------------------
1 |
51 |
52 |
BEAM Bloggers Webring
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/lib/webring/site.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.Site do
2 | @moduledoc """
3 |
4 | Generates list of sites from textfiles in sites folder
5 |
6 | """
7 | defstruct hash: nil, title: nil, url: nil, description: nil, fancy_body: nil
8 |
9 | alias Webring.Site
10 |
11 | @site_dir Path.join("priv", "sites")
12 | @site_files File.ls!(@site_dir)
13 | @sites @site_files
14 | |> Enum.map(fn filename ->
15 | data = File.read!(Path.join(@site_dir, filename))
16 | [url, title, blurb] = String.split(data, "\n\n", parts: 3)
17 | site_hash = :crypto.hash(:md5, filename <> data) |> Base.encode16()
18 |
19 | fancy =
20 | case Earmark.as_html(blurb) do
21 | {:ok, html, _} -> html
22 | _ -> nil
23 | end
24 |
25 | %{title: title, url: url, hash: site_hash, description: blurb, fancy_body: fancy}
26 | end)
27 | |> Enum.filter(fn %{url: url, fancy_body: fancy} ->
28 | uri = URI.parse(url)
29 | uri.scheme != nil and uri.host =~ "." and fancy
30 | end)
31 |
32 | # mark sites as external resources
33 | for site_file <- @site_files do
34 | @external_resource @site_dir |> Path.join(site_file) |> Path.relative_to_cwd()
35 | end
36 |
37 | def __mix_recompile__? do
38 | @site_dir |> File.ls!() |> :erlang.md5() !=
39 | :erlang.md5(@site_files)
40 | end
41 |
42 |
43 | def hash(filename, data) do
44 | :crypto.hash(:md5, filename <> data) |> Base.encode16()
45 | end
46 |
47 | # ordered list of sites determined at compile time, order should only change if logic changes
48 | # additions/deletions can happen but don't change the order
49 | def list_sites do
50 | @sites
51 | |> Enum.map(fn site ->
52 | Enum.reduce(site, %Site{}, fn {key, value}, site ->
53 | Map.put(site, key, value)
54 | end)
55 | end)
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/webring/feed_me/aggregate.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.FeedMe.Aggregate do
2 | @moduledoc """
3 |
4 | Uses elixir-rss to aggregate RSS feeds.
5 |
6 | """
7 | defstruct channel: nil, items: []
8 |
9 | require Logger
10 |
11 | alias Webring.FeedMe.Aggregate
12 |
13 | def new_feed(title, url, description, rfc1123_date, locale) do
14 | %Aggregate{
15 | channel: RSS.channel(title, url, description, rfc1123_date, locale),
16 | items: []
17 | }
18 | end
19 |
20 | def update_item(agg, guid, title, description, rfc1123_date, url, site_title) do
21 | # Should cover most cases where people have bad GUIDs
22 | guid = "#{url}---#{guid}"
23 | description = "From #{site_title}: #{description}"
24 |
25 | items =
26 | case rfc_to_iso(rfc1123_date) do
27 | {:ok, iso} ->
28 | items =
29 | Enum.reject(agg.items, fn {_iso, item_guid, _item} ->
30 | guid == item_guid
31 | end)
32 |
33 | rss_item = RSS.item(title, description, rfc1123_date, url, guid)
34 | item = {iso, guid, rss_item}
35 | [item | items]
36 |
37 | {:error, :parsing_failed} ->
38 | Logger.warn("Date parsing failed for: #{url} #{rfc1123_date} #{title}")
39 | agg.items
40 |
41 | _ ->
42 | agg.items
43 | end
44 |
45 | %{agg | items: items}
46 | end
47 |
48 | def render(agg) do
49 | items =
50 | agg.items
51 | |> Enum.sort()
52 | |> Enum.reverse()
53 | |> Enum.map(fn {_, _, item} -> item end)
54 |
55 | RSS.feed(agg.channel, items)
56 | end
57 |
58 | def rfc_to_iso(rfc_date) do
59 | case Timex.parse(rfc_date, "{RFC1123}") do
60 | {:ok, dt} ->
61 | Timex.format(dt, "{ISO:Extended}")
62 |
63 | {:error, "Expected `weekday abbreviation` at line 1, column 1."} ->
64 | # Assume ISO-8601, parse to fail if it doesn't work
65 | Timex.parse(rfc_date, "{ISO:Extended}")
66 |
67 | _ ->
68 | {:error, :parsing_failed}
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/webring_web/telemetry.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb.Telemetry do
2 | @moduledoc false
3 | use Supervisor
4 | import Telemetry.Metrics
5 |
6 | def start_link(arg) do
7 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
8 | end
9 |
10 | @impl true
11 | def init(_arg) do
12 | children = [
13 | # Telemetry poller will execute the given period measurements
14 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
15 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
16 | # Add reporters as children of your supervision tree.
17 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
18 | ]
19 |
20 | Supervisor.init(children, strategy: :one_for_one)
21 | end
22 |
23 | def metrics do
24 | [
25 | # Phoenix Metrics
26 | summary("phoenix.endpoint.stop.duration",
27 | unit: {:native, :millisecond}
28 | ),
29 | summary("phoenix.router_dispatch.stop.duration",
30 | tags: [:route],
31 | unit: {:native, :millisecond}
32 | ),
33 |
34 | # Database Metrics
35 | summary("webring.repo.query.total_time", unit: {:native, :millisecond}),
36 | summary("webring.repo.query.decode_time", unit: {:native, :millisecond}),
37 | summary("webring.repo.query.query_time", unit: {:native, :millisecond}),
38 | summary("webring.repo.query.queue_time", unit: {:native, :millisecond}),
39 | summary("webring.repo.query.idle_time", unit: {:native, :millisecond}),
40 |
41 | # VM Metrics
42 | summary("vm.memory.total", unit: {:byte, :kilobyte}),
43 | summary("vm.total_run_queue_lengths.total"),
44 | summary("vm.total_run_queue_lengths.cpu"),
45 | summary("vm.total_run_queue_lengths.io")
46 | ]
47 | end
48 |
49 | defp periodic_measurements do
50 | [
51 | # A module, function and arguments to be invoked periodically.
52 | # This function must call :telemetry.execute/3 and a metric must be added above.
53 | # {WebringWeb, :count_users, []}
54 | ]
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/webring_web.ex:
--------------------------------------------------------------------------------
1 | defmodule WebringWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use WebringWeb, :controller
9 | use WebringWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: WebringWeb
23 |
24 | import Plug.Conn
25 | import WebringWeb.Gettext
26 | alias WebringWeb.Router.Helpers, as: Routes
27 | end
28 | end
29 |
30 | def view do
31 | quote do
32 | use Phoenix.View,
33 | root: "lib/webring_web/templates",
34 | namespace: WebringWeb
35 |
36 | # Import convenience functions from controllers
37 | import Phoenix.Controller,
38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1]
39 |
40 | # Include shared imports and aliases for views
41 | unquote(view_helpers())
42 | end
43 | end
44 |
45 | def router do
46 | quote do
47 | use Phoenix.Router
48 |
49 | import Plug.Conn
50 | import Phoenix.Controller
51 | end
52 | end
53 |
54 | def channel do
55 | quote do
56 | use Phoenix.Channel
57 | import WebringWeb.Gettext
58 | end
59 | end
60 |
61 | defp view_helpers do
62 | quote do
63 | # Use all HTML functionality (forms, tags, etc)
64 | use Phoenix.HTML
65 |
66 | # Import basic rendering functionality (render, render_layout, etc)
67 | import Phoenix.View
68 |
69 | import WebringWeb.ErrorHelpers
70 | import WebringWeb.Gettext
71 | alias WebringWeb.Router.Helpers, as: Routes
72 | end
73 | end
74 |
75 | @doc """
76 | When used, dispatch to the appropriate controller/view/etc.
77 | """
78 | defmacro __using__(which) when is_atom(which) do
79 | apply(__MODULE__, which, [])
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :webring, WebringWeb.Endpoint,
13 | url: [host: "beambloggers.com", port: 20000],
14 | http: [port: 20000],
15 | cache_static_manifest: "priv/static/cache_manifest.json"
16 |
17 | # Do not print debug messages in production
18 | config :logger, level: :info
19 |
20 | # ## SSL Support
21 | #
22 | # To get SSL working, you will need to add the `https` key
23 | # to the previous section and set your `:url` port to 443:
24 | #
25 | # config :webring, WebringWeb.Endpoint,
26 | # ...
27 | # url: [host: "example.com", port: 443],
28 | # https: [
29 | # port: 443,
30 | # cipher_suite: :strong,
31 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
32 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
33 | # transport_options: [socket_opts: [:inet6]]
34 | # ]
35 | #
36 | # The `cipher_suite` is set to `:strong` to support only the
37 | # latest and more secure SSL ciphers. This means old browsers
38 | # and clients may not be supported. You can set it to
39 | # `:compatible` for wider support.
40 | #
41 | # `:keyfile` and `:certfile` expect an absolute path to the key
42 | # and cert in disk or a relative path inside priv, for example
43 | # "priv/ssl/server.key". For all supported SSL configuration
44 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
45 | #
46 | # We also recommend setting `force_ssl` in your endpoint, ensuring
47 | # no data is ever sent via http, always redirecting to https:
48 | #
49 | # config :webring, WebringWeb.Endpoint,
50 | # force_ssl: [hsts: true]
51 | #
52 | # Check `Plug.SSL` for all available options in `force_ssl`.
53 |
54 | # Finally import the config/prod.secret.exs which loads secrets
55 | # and configuration from environment variables.
56 | import_config "prod.secret.exs"
57 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Webring.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :webring,
7 | version: "0.1.0",
8 | elixir: "~> 1.11",
9 | elixirc_paths: elixirc_paths(Mix.env()),
10 | compilers: [:phoenix] ++ Mix.compilers(),
11 | start_permanent: Mix.env() == :prod,
12 | aliases: aliases(),
13 | deps: deps()
14 | ]
15 | end
16 |
17 | # Configuration for the OTP application.
18 | #
19 | # Type `mix help compile.app` for more information.
20 | def application do
21 | [
22 | mod: {Webring.Application, []},
23 | extra_applications: [:logger, :runtime_tools, :xmerl]
24 | ]
25 | end
26 |
27 | # Specifies which paths to compile per environment.
28 | defp elixirc_paths(:test), do: ["lib", "test/support"]
29 | defp elixirc_paths(_), do: ["lib"]
30 |
31 | # Specifies your project dependencies.
32 | #
33 | # Type `mix help deps` for examples and options.
34 | defp deps do
35 | [
36 | {:phoenix, "~> 1.5.6"},
37 | {:phoenix_ecto, "~> 4.1"},
38 | {:ecto_sql, "~> 3.4"},
39 | {:postgrex, ">= 0.0.0"},
40 | {:phoenix_html, "~> 2.11"},
41 | {:phoenix_live_reload, "~> 1.2", only: :dev},
42 | {:phoenix_live_dashboard, "~> 0.3 or ~> 0.2.9"},
43 | {:telemetry_metrics, "~> 0.4"},
44 | {:telemetry_poller, "~> 0.4"},
45 | {:gettext, "~> 0.11"},
46 | {:jason, "~> 1.0"},
47 | {:plug_cowboy, "~> 2.0"},
48 | {:earmark, "~> 1.4"},
49 | {:feeder_ex, "~> 1.1"},
50 | {:finch, "~> 0.5"},
51 | {:floki, "~> 0.29"},
52 | {:timex, "~> 3.0"},
53 | {:rss, github: "lawik/elixir-rss"},
54 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
55 | {:credo, "~> 1.5", only: [:dev, :test], runtime: false}
56 | ]
57 | end
58 |
59 | # Aliases are shortcuts or tasks specific to the current project.
60 | # For example, to install project dependencies and perform other setup tasks, run:
61 | #
62 | # $ mix setup
63 | #
64 | # See the documentation for `Mix` for more info on aliases.
65 | defp aliases do
66 | [
67 | "assets.deploy": ["esbuild default --minify", "phx.digest"],
68 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
69 | ]
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/old-cowboy/lib/webring.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring do
2 | require Logger
3 |
4 | @site_dir "priv/sites"
5 | @index_file "priv/generated/index.html"
6 | @style_file "priv/static/style.css"
7 | @integration_snippet "priv/integration/webring.min.html"
8 |
9 | def start(_type, _args) do
10 | dispatch_config = build_dispatch_config()
11 |
12 | port = System.get_env("PORT", "5555") |> String.to_integer()
13 | Logger.info("Starting on port: #{port}")
14 |
15 | Webring.generate_index_file()
16 |
17 | Webring.Supervisor.start_link(
18 | name: Webring.Supervisor,
19 | cowboy_args: [:http, [{:port, port}], %{env: %{dispatch: dispatch_config}}]
20 | )
21 | end
22 |
23 | def build_dispatch_config do
24 | :cowboy_router.compile([
25 | {:_,
26 | [
27 | {"/", :cowboy_static, {:file, @index_file}},
28 | {"/style.css", :cowboy_static, {:file, @style_file}},
29 | {"/shuffle", Webring.Handler, []},
30 | {"/integration", :cowboy_static, {:file, @integration_snippet}}
31 | ]}
32 | ])
33 | end
34 |
35 | def generate_index_file do
36 | contents =
37 | File.ls!(@site_dir)
38 | |> Enum.map(fn filename ->
39 | @site_dir
40 | |> Path.join(filename)
41 | |> File.read!()
42 | |> String.split("\n\n")
43 | |> site_to_html()
44 | end)
45 | |> Enum.join()
46 |
47 | contents = site_template(contents)
48 |
49 | File.mkdir_p!(Path.dirname(@index_file))
50 | File.write!(@index_file, contents)
51 | end
52 |
53 | defp site_to_html([url, title, blurb]) do
54 | """
55 |
56 |
#{title}
57 |
#{url}
58 |
#{blurb}
59 |
60 | """
61 | end
62 |
63 | defp site_to_html(site_data) do
64 | Logger.warn("Invalid site: #{site_data}")
65 | ""
66 | end
67 |
68 | defp site_template(contents) do
69 | """
70 |
71 |
72 | Beam Bloggers Webring
73 |
74 |
75 |
76 | Beam Bloggers Webring
77 | If you have a blog that regularly covers Elixir, Erlang, the BEAM or any related topics do join the webring .
78 | #{contents}
79 |
80 |
81 | """
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # Configure your database
4 | config :webring, Webring.Repo,
5 | username: "postgres",
6 | password: "postgres",
7 | database: "webring_dev",
8 | hostname: "localhost",
9 | show_sensitive_data_on_connection_error: true,
10 | pool_size: 10
11 |
12 | # For development, we disable any cache and enable
13 | # debugging and code reloading.
14 | #
15 | # The watchers configuration can be used to run external
16 | # watchers to your application.
17 | config :webring, WebringWeb.Endpoint,
18 | http: [port: 8989],
19 | debug_errors: true,
20 | code_reloader: true,
21 | check_origin: false,
22 | watchers: [
23 | esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
24 | ]
25 |
26 | # ## SSL Support
27 | #
28 | # In order to use HTTPS in development, a self-signed
29 | # certificate can be generated by running the following
30 | # Mix task:
31 | #
32 | # mix phx.gen.cert
33 | #
34 | # Note that this task requires Erlang/OTP 20 or later.
35 | # Run `mix help phx.gen.cert` for more information.
36 | #
37 | # The `http:` config above can be replaced with:
38 | #
39 | # https: [
40 | # port: 4001,
41 | # cipher_suite: :strong,
42 | # keyfile: "priv/cert/selfsigned_key.pem",
43 | # certfile: "priv/cert/selfsigned.pem"
44 | # ],
45 | #
46 | # If desired, both `http:` and `https:` keys can be
47 | # configured to run both http and https servers on
48 | # different ports.
49 |
50 | # Watch static and templates for browser reloading.
51 | config :webring, WebringWeb.Endpoint,
52 | live_reload: [
53 | patterns: [
54 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
55 | ~r"priv/gettext/.*(po)$",
56 | ~r"lib/webring_web/(live|views)/.*(ex)$",
57 | ~r"lib/webring_web/templates/.*(eex)$",
58 | ~r"priv/sites/*"
59 | ]
60 | ]
61 |
62 | # Do not include metadata nor timestamps in development logs
63 | config :logger, :console, format: "[$level] $message\n"
64 |
65 | # Set a higher stacktrace during development. Avoid configuring such
66 | # in production as building large stacktraces may be expensive.
67 | config :phoenix, :stacktrace_depth, 20
68 |
69 | # Initialize plugs at runtime for faster development compilation
70 | config :phoenix, :plug_init_mode, :runtime
71 |
72 | config :esbuild,
73 | version: "0.18.6",
74 | default: [
75 | args: ~w(js/app.js --bundle --target=es2016 --outdir=../priv/static/assets),
76 | cd: Path.expand("../assets", __DIR__),
77 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
78 | ]
79 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_acceptance/3
26 | msgid "must be accepted"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_format/3
30 | msgid "has invalid format"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_subset/3
34 | msgid "has an invalid entry"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_exclusion/3
38 | msgid "is reserved"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.validate_confirmation/3
42 | msgid "does not match confirmation"
43 | msgstr ""
44 |
45 | ## From Ecto.Changeset.no_assoc_constraint/3
46 | msgid "is still associated with this entry"
47 | msgstr ""
48 |
49 | msgid "are still associated with this entry"
50 | msgstr ""
51 |
52 | ## From Ecto.Changeset.validate_length/3
53 | msgid "should be %{count} character(s)"
54 | msgid_plural "should be %{count} character(s)"
55 | msgstr[0] ""
56 | msgstr[1] ""
57 |
58 | msgid "should have %{count} item(s)"
59 | msgid_plural "should have %{count} item(s)"
60 | msgstr[0] ""
61 | msgstr[1] ""
62 |
63 | msgid "should be at least %{count} character(s)"
64 | msgid_plural "should be at least %{count} character(s)"
65 | msgstr[0] ""
66 | msgstr[1] ""
67 |
68 | msgid "should have at least %{count} item(s)"
69 | msgid_plural "should have at least %{count} item(s)"
70 | msgstr[0] ""
71 | msgstr[1] ""
72 |
73 | msgid "should be at most %{count} character(s)"
74 | msgid_plural "should be at most %{count} character(s)"
75 | msgstr[0] ""
76 | msgstr[1] ""
77 |
78 | msgid "should have at most %{count} item(s)"
79 | msgid_plural "should have at most %{count} item(s)"
80 | msgstr[0] ""
81 | msgstr[1] ""
82 |
83 | ## From Ecto.Changeset.validate_number/3
84 | msgid "must be less than %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than %{number}"
88 | msgstr ""
89 |
90 | msgid "must be less than or equal to %{number}"
91 | msgstr ""
92 |
93 | msgid "must be greater than or equal to %{number}"
94 | msgstr ""
95 |
96 | msgid "must be equal to %{number}"
97 | msgstr ""
98 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here has no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_acceptance/3
24 | msgid "must be accepted"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_format/3
28 | msgid "has invalid format"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_subset/3
32 | msgid "has an invalid entry"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_exclusion/3
36 | msgid "is reserved"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.validate_confirmation/3
40 | msgid "does not match confirmation"
41 | msgstr ""
42 |
43 | ## From Ecto.Changeset.no_assoc_constraint/3
44 | msgid "is still associated with this entry"
45 | msgstr ""
46 |
47 | msgid "are still associated with this entry"
48 | msgstr ""
49 |
50 | ## From Ecto.Changeset.validate_length/3
51 | msgid "should be %{count} character(s)"
52 | msgid_plural "should be %{count} character(s)"
53 | msgstr[0] ""
54 | msgstr[1] ""
55 |
56 | msgid "should have %{count} item(s)"
57 | msgid_plural "should have %{count} item(s)"
58 | msgstr[0] ""
59 | msgstr[1] ""
60 |
61 | msgid "should be at least %{count} character(s)"
62 | msgid_plural "should be at least %{count} character(s)"
63 | msgstr[0] ""
64 | msgstr[1] ""
65 |
66 | msgid "should have at least %{count} item(s)"
67 | msgid_plural "should have at least %{count} item(s)"
68 | msgstr[0] ""
69 | msgstr[1] ""
70 |
71 | msgid "should be at most %{count} character(s)"
72 | msgid_plural "should be at most %{count} character(s)"
73 | msgstr[0] ""
74 | msgstr[1] ""
75 |
76 | msgid "should have at most %{count} item(s)"
77 | msgid_plural "should have at most %{count} item(s)"
78 | msgstr[0] ""
79 | msgstr[1] ""
80 |
81 | ## From Ecto.Changeset.validate_number/3
82 | msgid "must be less than %{number}"
83 | msgstr ""
84 |
85 | msgid "must be greater than %{number}"
86 | msgstr ""
87 |
88 | msgid "must be less than or equal to %{number}"
89 | msgstr ""
90 |
91 | msgid "must be greater than or equal to %{number}"
92 | msgstr ""
93 |
94 | msgid "must be equal to %{number}"
95 | msgstr ""
96 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Webring
2 |
3 | The Beam Bloggers webring is a collection of people and small companies blogging about Elixir, Erlang and other topics around the BEAM ecosystem. You find the site at [beambloggers.com](https://beambloggers.com).
4 |
5 | ## What is a Webring
6 |
7 | Webrings are an old school web thing that is having a small resurgence these days. The idea is that a bunch of sites with related subject matter join a (circular) list and they all include a small element to send traffic to the other parts of the Webring. Recently famed is the [Weird Wide Webring](https://weirdwidewebring.net/) that gathers interesting, odd and quirky sites. We'll be the next semi-medium-sized thing.
8 |
9 | We're currently not particularly circular and we need a new design for our UI to navigate to the next page in the ring. You can already create your own links though by pointing towards `/random`, `/prev` and `/next` on the beamblogger domain. Besides that we gather the sites and provide them to visitors :)
10 |
11 | ## How to join (or leave)
12 |
13 | Check out `priv/sites` and add your site following the way `underjord.txt` is laid out in a new file. Stick to a single paragraph of text. Submit it as a PR and we will take a look.
14 |
15 | Criteria:
16 | - Run your own site (not Medium, dev.to), if this seems bad, let us know in the issues
17 | - Your blog covers BEAM languages (Elixir, Erlang, Gleam, LFE and any others) or something closely related
18 | - There is no requirement for how often you post
19 |
20 | Recommendations:
21 | - Get an RSS feed if you don't have one already and add an RSS autodiscovery link if you don't have one
22 | - Add links on your site and point them to `/` (webring home), `/next`, `/prev` and `/random`
23 |
24 | If you want to leave or get rid of us, send a PR and we'll let you go :)
25 |
26 | ### RSS autodiscovery example
27 |
28 | We use these to pull your latest content and show it. It is an open standard. You control what we get and can show.
29 |
30 | ```
31 |
32 | ```
33 |
34 | ## Ordering of sites
35 |
36 | To allow as many people as possible to get a fair bit of exposure on this site we pull RSS feeds from the sites and expose the latest posts by each site. We also sort the main site list by latest posting. This means that posting often is an advantage for staying high in the site listing. However the "Latest from the feeds" section will only show a given site once regardless of their number of recent posts. We also only show the latest three under the site listing. The hope is that this gives a decent bit of fairness for active bloggers. It is documented here so if you wonder why you are not up in the rotation up top, you probably don't have an RSS feed and we want to encourage you to have one.
37 |
38 | We'll keep tweaking and tuning this functionality. There are some ideas about how to check freshness on non-RSS sites but RSS makes it easy so we start there. Suggestions and PRs are welcome.
39 |
40 | ## Running the thing
41 |
42 | To start your Phoenix server:
43 |
44 | * Install dependencies with `mix deps.get`
45 | * Start Phoenix endpoint with `mix phx.server`
46 |
47 | Now you can visit [`localhost:8989`](http://localhost:8989) from your browser.
48 |
--------------------------------------------------------------------------------
/old-cowboy/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "castore": {:hex, :castore, "0.1.8", "1b61eaba71bb755b756ac42d4741f4122f8beddb92456a84126d6177ec0af1fc", [:mix], [], "hexpm", "23ab8305baadb057bc689adc0088309f808cb2247dc9a48b87849bb1d242bb6c"},
3 | "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"},
4 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
5 | "earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"},
6 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
7 | "fast_rss": {:hex, :fast_rss, "0.3.4", "9e718934967a4b5e26e2fdefe48978265354a7ec81920ec5f2016640e8cfead5", [:mix], [{:rustler, "~> 0.21.0", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "fd44d814598f2d6f72ce29ef04693c3d001ca51e008baeaedc78866c88e92ea8"},
8 | "feeder": {:hex, :feeder, "2.3.2", "18792e18ae2928885e1859ecfbac6fc7a2ec6e99efa51828160e530b2a188c8b", [:make], [], "hexpm", "e176f1e486a4498ae0e235fb5d254082272d52e5f9bd825c469f1dcddf21edd4"},
9 | "feeder_ex": {:hex, :feeder_ex, "0.0.5", "1f877ed75a7a1277f7ed8a05cccf253dc19fc6b1927d1664a99fb6a9a153a1d1", [:mix], [{:feeder, "~> 2.0", [hex: :feeder, repo: "hexpm", optional: false]}], "hexpm", "7e12017e51bcec54aa20381d45311d3c7bef7d8c33d9d81272db269607aaf6c7"},
10 | "finch": {:hex, :finch, "0.5.1", "2d8754b2ea3c629574f11a37d9cf00ed54807dfa55fe1cf755fab9222e973a7c", [:mix], [{:castore, "~> 0.1.5", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.2", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.3", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5f8f6f83f21e6a9076eac80c26391507c34e45b59c04a133c23f9e023eb5485f"},
11 | "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"},
12 | "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
13 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
14 | "mint": {:hex, :mint, "1.2.0", "65e9d75c60c456a5fb1b800febb88f061f56157d103d755b99fcaeaeb3e956f3", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "19cbb3a5be91b7df4a35377ba94b26199481a541add055cf5d1d4299b55125ab"},
15 | "nimble_options": {:hex, :nimble_options, "0.3.3", "49f52786980c371435bab03246392dfa32f49738344f27662838566d5276b0de", [:mix], [], "hexpm", "79db909818900c5469616b4db25c5c194daeebf4b76ad324e434d9c3172ce0bd"},
16 | "nimble_pool": {:hex, :nimble_pool, "0.2.3", "4b84df87cf8b40c7363782a99faad6aa2bb0811bcd3d275b5402ae4bab1f1251", [:mix], [], "hexpm", "a6bf677d3499ef1639c42bf16b8a72bf490f5fed70206d5851d43dd750c7eaca"},
17 | "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [: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", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
18 | "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [: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", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"},
19 | "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
20 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
21 | "rustler": {:hex, :rustler, "0.21.1", "5299980be32da997c54382e945bacaa015ed97a60745e1e639beaf6a7b278c65", [:mix], [{:toml, "~> 0.5.2", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "6ee1651e10645b2b2f3bb70502bf180341aa058709177e9bc28c105934094bc6"},
22 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
23 | "toml": {:hex, :toml, "0.5.2", "e471388a8726d1ce51a6b32f864b8228a1eb8edc907a0edf2bb50eab9321b526", [:mix], [], "hexpm", "f1e3dabef71fb510d015fad18c0e05e7c57281001141504c6b69d94e99750a07"},
24 | }
25 |
--------------------------------------------------------------------------------
/lib/webring/feed_me.ex:
--------------------------------------------------------------------------------
1 | defmodule Webring.FeedMe do
2 | @moduledoc """
3 |
4 | Collects RSS feeds from member sites and put them together in one single feed.
5 |
6 | """
7 | use GenServer
8 |
9 | require Logger
10 |
11 | alias Webring.FeedMe.Aggregate
12 |
13 | # hourly
14 | @check_interval 1000 * 60 * 60
15 |
16 | # API
17 | def start_link(_) do
18 | GenServer.start_link(Webring.FeedMe, nil, name: Webring.FeedMe)
19 | end
20 |
21 | def list_feeds do
22 | GenServer.call(Webring.FeedMe, :list)
23 | end
24 |
25 | # GenServer Implementation
26 | def get_rss do
27 | GenServer.call(Webring.FeedMe, :rss)
28 | end
29 |
30 | @impl true
31 | def init(nil) do
32 | sites = Webring.Site.list_sites()
33 |
34 | feed_datetime =
35 | "Etc/UTC"
36 | |> Timex.now()
37 | |> Timex.format!("{RFC1123}")
38 |
39 | aggregated_feed =
40 | Aggregate.new_feed(
41 | "Beambloggers Webring",
42 | "https://beambloggers.com/feed",
43 | "A collection of our webring members posts in an aggregated RSS feed. Please check out their blog posts :)",
44 | feed_datetime,
45 | "en-us"
46 | )
47 |
48 | state = %{
49 | sites: sites,
50 | feeds: %{},
51 | feed_data: aggregated_feed,
52 | feed_string: Aggregate.render(aggregated_feed)
53 | }
54 |
55 | state = check(state)
56 | schedule_check()
57 | {:ok, state}
58 | end
59 |
60 | @impl true
61 | def handle_call(:list, _from, state) do
62 | {:reply, state.feeds, state}
63 | end
64 |
65 | @impl true
66 | def handle_call(:rss, _from, state) do
67 | {:reply, state.feed_string, state}
68 | end
69 |
70 | @impl true
71 | def handle_info(:check, state) do
72 | state = check(state)
73 | schedule_check()
74 | {:noreply, state}
75 | end
76 |
77 | @impl true
78 | def handle_info({:update, hash, feed}, %{feeds: feeds, feed_data: agg} = state) do
79 | agg =
80 | Enum.reduce(feed.items, agg, fn item, agg ->
81 | Aggregate.update_item(
82 | agg,
83 | item.guid,
84 | item.title,
85 | item.description,
86 | item.rfc_datetime,
87 | item.url,
88 | feed.title
89 | )
90 | end)
91 |
92 | feeds = Map.put(feeds, hash, feed)
93 | feed_render = Aggregate.render(agg)
94 | {:noreply, %{state | feeds: feeds, feed_data: agg, feed_string: feed_render}}
95 | end
96 |
97 | def schedule_check() do
98 | Process.send_after(self(), :check, @check_interval)
99 | end
100 |
101 | def check_rss(site) do
102 | {:ok, body} = request_url(site.url, site.url)
103 |
104 | Logger.info("URL: #{site.url}")
105 |
106 | with {:ok, rss_url} <- find_rss(body, site.url),
107 | {:ok, rss_feed_body} <- request_url(site.url, rss_url),
108 | {:ok, feed} <- parse_feed(rss_feed_body) do
109 | Logger.info("RSS URL: #{rss_url}")
110 | Logger.info("Parsing seems okay, found title: #{feed.title}")
111 |
112 | # entries
113 | ## title, description, pub_date, link, guid
114 | entries =
115 | Enum.map(feed.entries, fn item ->
116 | {datetime, iso} =
117 | case Timex.parse(item.updated, "{RFC1123}") do
118 | {:ok, dt} ->
119 | {dt, Timex.format!(dt, "{ISO:Extended}")}
120 |
121 | {:error, "Expected `weekday abbreviation` at line 1, column 1."} ->
122 | {NaiveDateTime.from_iso8601!(item.updated), item.updated}
123 |
124 | _ ->
125 | {nil, nil}
126 | end
127 |
128 | %{
129 | title: item.title,
130 | guid: item.id,
131 | description: item.summary,
132 | rfc_datetime: item.updated,
133 | datetime: datetime,
134 | iso_datetime: iso,
135 | url: item.link
136 | }
137 | end)
138 | |> Enum.filter(fn item ->
139 | item.datetime
140 | end)
141 | |> Enum.sort_by(
142 | fn item ->
143 | item.iso_datetime
144 | end,
145 | :desc
146 | )
147 |
148 | entry_count = Enum.count(entries)
149 | Logger.info("Items parse: #{entry_count}")
150 | %{title: feed.title, items: entries}
151 | else
152 | {:error, :rss_error} ->
153 | Logger.info("No RSS URL found")
154 | nil
155 |
156 | {:error, :parse_error} ->
157 | nil
158 |
159 | {:error, :url_error} ->
160 | nil
161 | end
162 | end
163 |
164 | defp check(%{sites: sites} = state) do
165 | pid = self()
166 |
167 | Enum.each(sites, fn site ->
168 | Task.start(fn ->
169 | result = check_rss(site)
170 | result && Process.send_after(pid, {:update, site.hash, result}, 1)
171 | end)
172 | end)
173 |
174 | state
175 | end
176 |
177 | defp parse_feed(feed) do
178 | case FeederEx.parse(feed) do
179 | {:ok, rss, rest} ->
180 | unless String.trim(rest) == "" do
181 | Logger.info("Feed contains additional data: #{inspect(rest)}")
182 | end
183 |
184 | {:ok, rss}
185 |
186 | error ->
187 | Logger.info("An error occurred in feed parsing: #{inspect(error)}")
188 | {:ok, :parse_error}
189 | end
190 | end
191 |
192 | defp find_rss(body, url) do
193 | case Floki.parse_document(body) do
194 | {:ok, doc} ->
195 | rss_url = find_rss_link(doc, url )
196 |
197 | {:ok, rss_url}
198 |
199 | error ->
200 | Logger.info("Parsing failed for URL #{url}: #{inspect(error)}")
201 | {:error, :rss_error}
202 | end
203 | end
204 |
205 | defp find_rss_link(doc, url) do
206 | with [] <- Floki.find(doc, "link[type=\"application/rss+xml\"") do
207 | Floki.find(doc, "link[type=\"application/atom+xml\"")
208 | end
209 | |> Enum.find_value(fn link ->
210 | extract_rss_url(link, url)
211 | end)
212 | end
213 |
214 | defp extract_rss_url({_link, attrs, _}, url) do
215 | Enum.find_value(attrs, fn {key, value} ->
216 | # Some are relative, fix that
217 | if key == "href", do: URI.merge(url, value), else: nil
218 | end)
219 | end
220 |
221 | defp request_url(_base_url, nil) do
222 | {:error, :url_error}
223 | end
224 |
225 | defp request_url(base_url, url) do
226 | request = Finch.build(:get, url)
227 |
228 | case Finch.request(request, :default) do
229 | {:ok, %{status: 302, headers: headers}} ->
230 | Logger.info("Redirect 302 at: #{url}")
231 | handle_redirect(base_url, url, headers)
232 |
233 | {:ok, %{status: 301, headers: headers}} ->
234 | Logger.info("Redirect 301 at: #{url}")
235 | handle_redirect(base_url, url, headers)
236 |
237 | {:ok, response} ->
238 | {:ok, response.body}
239 |
240 | error ->
241 | Logger.info("Failed to request URL #{url}, error: #{inspect(error)}")
242 | {:error, :url_error}
243 | end
244 | end
245 |
246 | defp handle_redirect(base_url, url, headers) do
247 | new_url =
248 | Enum.find_value(headers, fn {key, value} ->
249 | key == "location" && value
250 | end)
251 |
252 | if is_nil(new_url) do
253 | ""
254 | else
255 | new_url =
256 | if String.starts_with?(new_url, "http") do
257 | new_url
258 | else
259 | pre = String.trim_trailing(base_url, "/")
260 | post = String.trim_leading(new_url, "/")
261 | pre <> "/" <> post
262 | end
263 |
264 | if new_url == url do
265 | ""
266 | else
267 | request_url(base_url, new_url)
268 | end
269 | end
270 | end
271 | end
272 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3 | "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"},
4 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
5 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
6 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
7 | "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
8 | "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"},
9 | "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
10 | "credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"},
11 | "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
12 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
13 | "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
14 | "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
15 | "ecto": {:hex, :ecto, "3.11.1", "4b4972b717e7ca83d30121b12998f5fcdc62ba0ed4f20fd390f16f3270d85c3e", [:mix], [{:decimal, "~> 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", "ebd3d3772cd0dfcd8d772659e41ed527c28b2a8bde4b00fe03e0463da0f1983b"},
16 | "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.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", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
17 | "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
18 | "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"},
19 | "feeder": {:hex, :feeder, "2.3.2", "18792e18ae2928885e1859ecfbac6fc7a2ec6e99efa51828160e530b2a188c8b", [:make], [], "hexpm", "e176f1e486a4498ae0e235fb5d254082272d52e5f9bd825c469f1dcddf21edd4"},
20 | "feeder_ex": {:hex, :feeder_ex, "1.1.0", "0be3732255cdb45dec949e0ede6852b5261c9ff173360e8274a6ac65183b2b55", [:mix], [{:feeder, "~> 2.2", [hex: :feeder, repo: "hexpm", optional: false]}], "hexpm", "3d06dfcc3e13a2fb66182dffc16e112b0ef9aea432a949f48b56c667ac09d07e"},
21 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
22 | "finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"},
23 | "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
24 | "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
25 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.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.4.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", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
26 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
27 | "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
28 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
29 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
30 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
31 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
32 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
33 | "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
34 | "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
35 | "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
36 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
37 | "phoenix": {:hex, :phoenix, "1.5.14", "2d5db884be496eefa5157505ec0134e66187cb416c072272420c5509d67bf808", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [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 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "207f1aa5520320cbb7940d7ff2dde2342162cf513875848f88249ea0ba02fef7"},
38 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"},
39 | "phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
40 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.4.0", "87990e68b60213d7487e65814046f9a2bed4a67886c943270125913499b3e5c3", [:mix], [{:ecto_psql_extras, "~> 0.4.1 or ~> 0.5", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0 or ~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "8d52149e58188e9e4497cc0d8900ab94d9b66f96998ec38c47c7a4f8f4f50e57"},
41 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [: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", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
42 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.15.7", "09720b8e5151b3ca8ef739cd7626d4feb987c69ba0b509c9bbdb861d5a365881", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 0.5", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a756cf662420272d0f1b3b908cce5222163b5a95aa9bab404f9d29aff53276e"},
43 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
44 | "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
45 | "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
46 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
47 | "postgrex": {:hex, :postgrex, "0.17.4", "5777781f80f53b7c431a001c8dad83ee167bcebcf3a793e3906efff680ab62b3", [: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", "6458f7d5b70652bc81c3ea759f91736c16a31be000f306d3c64bcdfe9a18b3cc"},
48 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
49 | "rss": {:git, "https://github.com/lawik/elixir-rss.git", "37b555ac95fcfed931a9e23c3ccda2628c718da2", []},
50 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
51 | "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
52 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.2", "2caabe9344ec17eafe5403304771c3539f3b6e2f7fb6a6f602558c825d0d0bfb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9b43db0dc33863930b9ef9d27137e78974756f5f198cae18409970ed6fa5b561"},
53 | "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
54 | "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
55 | "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
56 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
57 | }
58 |
--------------------------------------------------------------------------------