├── 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 | 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 | 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 |
BEAM Bloggers Webring
-------------------------------------------------------------------------------- /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 | 21 | 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 | --------------------------------------------------------------------------------