Compare commits
23 Commits
0cf90d0bea
...
feature/be
Author | SHA1 | Date | |
---|---|---|---|
24b0942a13 | |||
256c883488 | |||
6d64449c1b | |||
6751ade1d9 | |||
bf7f1a4601 | |||
bab66c8079 | |||
914246f08a | |||
0c52c18d92 | |||
cb66d127b6 | |||
ce5a011d90 | |||
fede012e44 | |||
2ab3f7ba7f | |||
8383d00335 | |||
1c1d218c82 | |||
4cc2ee8276 | |||
36d97eebec | |||
4a25eae6c6 | |||
c29ae7f3e4 | |||
757c1fe063 | |||
1829ea4c8a | |||
f1aba90737 | |||
58605fb1a2 | |||
e20a321cf9 |
217
.credo.exs
Normal file
217
.credo.exs
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# This file contains the configuration for Credo and you are probably reading
|
||||||
|
# this after creating it with `mix credo.gen.config`.
|
||||||
|
#
|
||||||
|
# If you find anything wrong or unclear in this file, please report an
|
||||||
|
# issue on GitHub: https://github.com/rrrene/credo/issues
|
||||||
|
#
|
||||||
|
%{
|
||||||
|
#
|
||||||
|
# You can have as many configs as you like in the `configs:` field.
|
||||||
|
configs: [
|
||||||
|
%{
|
||||||
|
#
|
||||||
|
# Run any config using `mix credo -C <name>`. If no config name is given
|
||||||
|
# "default" is used.
|
||||||
|
#
|
||||||
|
name: "default",
|
||||||
|
#
|
||||||
|
# These are the files included in the analysis:
|
||||||
|
files: %{
|
||||||
|
#
|
||||||
|
# You can give explicit globs or simply directories.
|
||||||
|
# In the latter case `**/*.{ex,exs}` will be used.
|
||||||
|
#
|
||||||
|
included: [
|
||||||
|
"lib/",
|
||||||
|
"src/",
|
||||||
|
"test/",
|
||||||
|
"web/",
|
||||||
|
"apps/*/lib/",
|
||||||
|
"apps/*/src/",
|
||||||
|
"apps/*/test/",
|
||||||
|
"apps/*/web/"
|
||||||
|
],
|
||||||
|
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
||||||
|
},
|
||||||
|
#
|
||||||
|
# Load and configure plugins here:
|
||||||
|
#
|
||||||
|
plugins: [],
|
||||||
|
#
|
||||||
|
# If you create your own checks, you must specify the source files for
|
||||||
|
# them here, so they can be loaded by Credo before running the analysis.
|
||||||
|
#
|
||||||
|
requires: [],
|
||||||
|
#
|
||||||
|
# If you want to enforce a style guide and need a more traditional linting
|
||||||
|
# experience, you can change `strict` to `true` below:
|
||||||
|
#
|
||||||
|
strict: true,
|
||||||
|
#
|
||||||
|
# To modify the timeout for parsing files, change this value:
|
||||||
|
#
|
||||||
|
parse_timeout: 5000,
|
||||||
|
#
|
||||||
|
# If you want to use uncolored output by default, you can change `color`
|
||||||
|
# to `false` below:
|
||||||
|
#
|
||||||
|
color: true,
|
||||||
|
#
|
||||||
|
# You can customize the parameters of any check by adding a second element
|
||||||
|
# to the tuple.
|
||||||
|
#
|
||||||
|
# To disable a check put `false` as second element:
|
||||||
|
#
|
||||||
|
# {Credo.Check.Design.DuplicatedCode, false}
|
||||||
|
#
|
||||||
|
checks: %{
|
||||||
|
enabled: [
|
||||||
|
#
|
||||||
|
## Consistency Checks
|
||||||
|
#
|
||||||
|
{Credo.Check.Consistency.ExceptionNames, []},
|
||||||
|
{Credo.Check.Consistency.LineEndings, []},
|
||||||
|
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||||
|
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||||
|
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||||
|
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Design Checks
|
||||||
|
#
|
||||||
|
# You can customize the priority of any check
|
||||||
|
# Priority values are: `low, normal, high, higher`
|
||||||
|
#
|
||||||
|
{Credo.Check.Design.AliasUsage,
|
||||||
|
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
|
||||||
|
{Credo.Check.Design.TagFIXME, []},
|
||||||
|
# You can also customize the exit_status of each check.
|
||||||
|
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||||
|
# set this value to 0 (zero).
|
||||||
|
#
|
||||||
|
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Readability Checks
|
||||||
|
#
|
||||||
|
{Credo.Check.Readability.AliasOrder, []},
|
||||||
|
{Credo.Check.Readability.FunctionNames, []},
|
||||||
|
{Credo.Check.Readability.LargeNumbers, []},
|
||||||
|
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||||
|
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||||
|
{Credo.Check.Readability.ModuleDoc, []},
|
||||||
|
{Credo.Check.Readability.ModuleNames, []},
|
||||||
|
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||||
|
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||||
|
{Credo.Check.Readability.PipeIntoAnonymousFunctions, []},
|
||||||
|
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||||
|
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||||
|
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||||
|
{Credo.Check.Readability.Semicolons, []},
|
||||||
|
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||||
|
{Credo.Check.Readability.StringSigils, []},
|
||||||
|
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||||
|
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||||
|
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||||
|
{Credo.Check.Readability.VariableNames, []},
|
||||||
|
{Credo.Check.Readability.WithSingleClause, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Refactoring Opportunities
|
||||||
|
#
|
||||||
|
{Credo.Check.Refactor.Apply, []},
|
||||||
|
{Credo.Check.Refactor.CondStatements, []},
|
||||||
|
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||||
|
{Credo.Check.Refactor.FilterCount, []},
|
||||||
|
{Credo.Check.Refactor.FilterFilter, []},
|
||||||
|
{Credo.Check.Refactor.FunctionArity, []},
|
||||||
|
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||||
|
{Credo.Check.Refactor.MapJoin, []},
|
||||||
|
{Credo.Check.Refactor.MatchInCondition, []},
|
||||||
|
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||||
|
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||||
|
{Credo.Check.Refactor.Nesting, []},
|
||||||
|
{Credo.Check.Refactor.RedundantWithClauseResult, []},
|
||||||
|
{Credo.Check.Refactor.RejectReject, []},
|
||||||
|
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||||
|
{Credo.Check.Refactor.WithClauses, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Warnings
|
||||||
|
#
|
||||||
|
{Credo.Check.Warning.ApplicationConfigInModuleAttribute, []},
|
||||||
|
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||||
|
{Credo.Check.Warning.Dbg, []},
|
||||||
|
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||||
|
{Credo.Check.Warning.IExPry, []},
|
||||||
|
{Credo.Check.Warning.IoInspect, []},
|
||||||
|
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []},
|
||||||
|
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||||
|
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||||
|
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||||
|
{Credo.Check.Warning.SpecWithStruct, []},
|
||||||
|
{Credo.Check.Warning.UnsafeExec, []},
|
||||||
|
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedListOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||||
|
{Credo.Check.Warning.WrongTestFileExtension, []}
|
||||||
|
],
|
||||||
|
disabled: [
|
||||||
|
#
|
||||||
|
# Checks scheduled for next check update (opt-in for now)
|
||||||
|
{Credo.Check.Refactor.UtcNowTruncate, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
|
||||||
|
# and be sure to use `mix credo --strict` to see low priority checks)
|
||||||
|
#
|
||||||
|
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
|
||||||
|
{Credo.Check.Consistency.UnusedVariableNames, []},
|
||||||
|
{Credo.Check.Design.DuplicatedCode, []},
|
||||||
|
{Credo.Check.Design.SkipTestWithoutComment, []},
|
||||||
|
{Credo.Check.Readability.AliasAs, []},
|
||||||
|
{Credo.Check.Readability.BlockPipe, []},
|
||||||
|
{Credo.Check.Readability.ImplTrue, []},
|
||||||
|
{Credo.Check.Readability.MultiAlias, []},
|
||||||
|
{Credo.Check.Readability.NestedFunctionCalls, []},
|
||||||
|
{Credo.Check.Readability.OneArityFunctionInPipe, []},
|
||||||
|
{Credo.Check.Readability.OnePipePerLine, []},
|
||||||
|
{Credo.Check.Readability.SeparateAliasRequire, []},
|
||||||
|
{Credo.Check.Readability.SingleFunctionToBlockPipe, []},
|
||||||
|
{Credo.Check.Readability.SinglePipe, []},
|
||||||
|
{Credo.Check.Readability.Specs, []},
|
||||||
|
{Credo.Check.Readability.StrictModuleLayout, []},
|
||||||
|
{Credo.Check.Readability.WithCustomTaggedTuple, []},
|
||||||
|
{Credo.Check.Refactor.ABCSize, []},
|
||||||
|
{Credo.Check.Refactor.AppendSingleItem, []},
|
||||||
|
{Credo.Check.Refactor.DoubleBooleanNegation, []},
|
||||||
|
{Credo.Check.Refactor.FilterReject, []},
|
||||||
|
{Credo.Check.Refactor.IoPuts, []},
|
||||||
|
{Credo.Check.Refactor.MapMap, []},
|
||||||
|
{Credo.Check.Refactor.ModuleDependencies, []},
|
||||||
|
{Credo.Check.Refactor.NegatedIsNil, []},
|
||||||
|
{Credo.Check.Refactor.PassAsyncInTestCases, []},
|
||||||
|
{Credo.Check.Refactor.PipeChainStart, []},
|
||||||
|
{Credo.Check.Refactor.RejectFilter, []},
|
||||||
|
{Credo.Check.Refactor.VariableRebinding, []},
|
||||||
|
{Credo.Check.Warning.LazyLogging, []},
|
||||||
|
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||||
|
{Credo.Check.Warning.MapGetUnsafePass, []},
|
||||||
|
{Credo.Check.Warning.MixEnv, []},
|
||||||
|
{Credo.Check.Warning.UnsafeToAtom, []}
|
||||||
|
|
||||||
|
# {Credo.Check.Refactor.MapInto, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom checks can be created using `mix credo.gen.check`.
|
||||||
|
#
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -21,7 +21,7 @@ ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
|
|||||||
FROM ${BUILDER_IMAGE} as builder
|
FROM ${BUILDER_IMAGE} as builder
|
||||||
|
|
||||||
# install build dependencies
|
# install build dependencies
|
||||||
RUN apt-get update -y && apt-get install -y build-essential git \
|
RUN apt-get update -y && apt-get install -y build-essential git npm \
|
||||||
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
|
&& apt-get clean && rm -f /var/lib/apt/lists/*_*
|
||||||
|
|
||||||
# prepare build dir
|
# prepare build dir
|
||||||
@ -51,6 +51,8 @@ COPY lib lib
|
|||||||
|
|
||||||
COPY assets assets
|
COPY assets assets
|
||||||
|
|
||||||
|
RUN npm ci --prefix=./assets/
|
||||||
|
|
||||||
# compile assets
|
# compile assets
|
||||||
RUN mix assets.deploy
|
RUN mix assets.deploy
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ const plugin = require("tailwindcss/plugin")
|
|||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
|
|
||||||
|
import daisyui from "daisyui"
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"./js/**/*.js",
|
"./js/**/*.js",
|
||||||
@ -24,7 +26,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require("@tailwindcss/forms"),
|
require("@tailwindcss/forms"),
|
||||||
require("daisyui"),
|
daisyui,
|
||||||
// Allows prefixing tailwind classes with LiveView classes to add rules
|
// Allows prefixing tailwind classes with LiveView classes to add rules
|
||||||
// only when LiveView classes are applied, for example:
|
// only when LiveView classes are applied, for example:
|
||||||
//
|
//
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
defmodule SomethingErlang.AwfulApi do
|
defmodule SomethingErlang.AwfulApi do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias SomethingErlang.AwfulApi.Thread
|
alias SomethingErlang.AwfulApi.{Client, Thread, Bookmarks}
|
||||||
alias SomethingErlang.AwfulApi.Bookmarks
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of all posts on page of a thread.
|
Returns a list of all posts on page of a thread.
|
||||||
@ -16,10 +15,12 @@ defmodule SomethingErlang.AwfulApi do
|
|||||||
12
|
12
|
||||||
"""
|
"""
|
||||||
def parsed_thread(id, page, user) do
|
def parsed_thread(id, page, user) do
|
||||||
Thread.compile(id, page, user)
|
Client.thread_doc(id, page, user)
|
||||||
|
|> Thread.compile()
|
||||||
end
|
end
|
||||||
|
|
||||||
def bookmarks(user) do
|
def bookmarks(page, user) do
|
||||||
Bookmarks.compile(1, user)
|
Client.bookmarks_doc(page, user)
|
||||||
|
|> Bookmarks.compile()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,59 +1,84 @@
|
|||||||
defmodule SomethingErlang.AwfulApi.Bookmarks do
|
defmodule SomethingErlang.AwfulApi.Bookmarks do
|
||||||
|
import Meeseeks.CSS
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias SomethingErlang.AwfulApi.Client
|
def compile(html) do
|
||||||
|
for thread <- Meeseeks.all(html, css("tr.thread")) do
|
||||||
|
thread_id =
|
||||||
|
Meeseeks.attr(thread, "id")
|
||||||
|
|> String.split("thread")
|
||||||
|
|> List.last()
|
||||||
|
|> String.to_integer()
|
||||||
|
|
||||||
def compile(page, user) do
|
|
||||||
doc = Client.bookmarks_doc(page, user)
|
|
||||||
html = Floki.parse_document!(doc)
|
|
||||||
|
|
||||||
for thread <- Floki.find(html, "tr.thread") do
|
|
||||||
parse(thread)
|
parse(thread)
|
||||||
|
|> Map.put(:id, thread_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(thread) do
|
def parse(thread) do
|
||||||
%{
|
for col <- Meeseeks.all(thread, css("td:not(.star)")),
|
||||||
title: Floki.find(thread, "td.title") |> inner_html() |> Floki.raw_html(),
|
class = Meeseeks.attr(col, "class") |> String.split() |> List.first(),
|
||||||
icon: Floki.find(thread, "td.icon") |> inner_html() |> Floki.raw_html(),
|
|
||||||
author: Floki.find(thread, "td.author") |> inner_html() |> Floki.text(),
|
|
||||||
replies: Floki.find(thread, "td.replies") |> inner_html() |> Floki.text(),
|
|
||||||
views: Floki.find(thread, "td.views") |> inner_html() |> Floki.text(),
|
|
||||||
rating: Floki.find(thread, "td.rating") |> inner_html() |> Floki.raw_html(),
|
|
||||||
lastpost: Floki.find(thread, "td.lastpost") |> inner_html() |> Floki.raw_html()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {"td", [{"class", class} | _attrs], children} <- Floki.find(thread, "td"),
|
|
||||||
String.starts_with?(class, "star") == false,
|
|
||||||
into: %{} do
|
into: %{} do
|
||||||
case class do
|
{String.to_atom(class), thread_data(class, col)}
|
||||||
<<"title", _rest::binary>> ->
|
|
||||||
{:title, children |> Floki.raw_html()}
|
|
||||||
|
|
||||||
<<"icon", _rest::binary>> ->
|
|
||||||
{:icon, children |> Floki.raw_html()}
|
|
||||||
|
|
||||||
<<"author", _rest::binary>> ->
|
|
||||||
{:author, children |> Floki.text()}
|
|
||||||
|
|
||||||
<<"replies", _rest::binary>> ->
|
|
||||||
{:replies, children |> Floki.text() |> String.to_integer()}
|
|
||||||
|
|
||||||
<<"views", _rest::binary>> ->
|
|
||||||
{:views, children |> Floki.text() |> String.to_integer()}
|
|
||||||
|
|
||||||
<<"rating", _rest::binary>> ->
|
|
||||||
{:rating, children |> Floki.raw_html()}
|
|
||||||
|
|
||||||
<<"lastpost", _rest::binary>> ->
|
|
||||||
{:lastpost, children |> Floki.raw_html()}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp thread_data("icon", td) do
|
||||||
|
img = Meeseeks.one(td, css("img"))
|
||||||
|
|
||||||
|
%{
|
||||||
|
icon: Meeseeks.attr(img, "src"),
|
||||||
|
title: Meeseeks.attr(img, "alt")
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp inner_html(node) do
|
defp thread_data("title", td) do
|
||||||
node
|
last_seen = Meeseeks.one(td, css(".lastseen .count"))
|
||||||
|> List.first()
|
info = Meeseeks.one(td, css(".info"))
|
||||||
|> Floki.children()
|
|
||||||
|
%{
|
||||||
|
new_posts:
|
||||||
|
case Meeseeks.text(last_seen) do
|
||||||
|
nil -> 0
|
||||||
|
n -> String.to_integer(n)
|
||||||
|
end,
|
||||||
|
thread_title: Meeseeks.text(Meeseeks.one(info, css(".thread_title")))
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp thread_data("author", td) do
|
||||||
|
Meeseeks.text(td)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp thread_data("replies", td) do
|
||||||
|
Meeseeks.text(td)
|
||||||
|
|> String.to_integer()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp thread_data("views", td) do
|
||||||
|
Meeseeks.text(td)
|
||||||
|
|> String.to_integer()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp thread_data("rating", td) do
|
||||||
|
img = Meeseeks.one(td, css("img"))
|
||||||
|
|
||||||
|
%{
|
||||||
|
icon: Meeseeks.attr(img, "src"),
|
||||||
|
title: Meeseeks.attr(img, "title")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp thread_data("lastpost", td) do
|
||||||
|
date = Meeseeks.one(td, css(".date"))
|
||||||
|
author = Meeseeks.one(td, css(".author"))
|
||||||
|
|
||||||
|
%{
|
||||||
|
date: Meeseeks.text(date),
|
||||||
|
author: Meeseeks.text(author)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp thread_data(class, _td), do: Logger.error("#{inspect(class)} not found")
|
||||||
end
|
end
|
||||||
|
@ -66,7 +66,7 @@ defmodule SomethingErlang.AwfulApi.Client do
|
|||||||
Req.new(
|
Req.new(
|
||||||
base_url: @base_url,
|
base_url: @base_url,
|
||||||
user_agent: @user_agent,
|
user_agent: @user_agent,
|
||||||
cache: true,
|
cache: false,
|
||||||
headers: [cookie: [cookies(%{bbuserid: user.id, bbpassword: user.hash})]]
|
headers: [cookie: [cookies(%{bbuserid: user.id, bbpassword: user.hash})]]
|
||||||
)
|
)
|
||||||
|> Req.Request.append_request_steps(
|
|> Req.Request.append_request_steps(
|
||||||
|
@ -1,39 +1,60 @@
|
|||||||
defmodule SomethingErlang.AwfulApi.Thread do
|
defmodule SomethingErlang.AwfulApi.Thread do
|
||||||
|
import Meeseeks.CSS
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
alias SomethingErlang.AwfulApi.Client
|
def compile(html) do
|
||||||
|
title =
|
||||||
|
Meeseeks.one(html, css("title"))
|
||||||
|
|> Meeseeks.text()
|
||||||
|
|> String.replace(" - The Something Awful Forums", "")
|
||||||
|
|
||||||
def compile(id, page, user) do
|
thread =
|
||||||
doc = Client.thread_doc(id, page, user)
|
Meeseeks.one(html, css("#thread"))
|
||||||
html = Floki.parse_document!(doc)
|
|
||||||
thread = Floki.find(html, "#thread") |> Floki.filter_out("table.post.ignored")
|
|
||||||
|
|
||||||
title = Floki.find(html, "title") |> Floki.text()
|
thread_id =
|
||||||
title = title |> String.replace(" - The Something Awful Forums", "")
|
Meeseeks.attr(thread, "class")
|
||||||
|
|> String.split(":")
|
||||||
|
|> List.last()
|
||||||
|
|> String.to_integer()
|
||||||
|
|
||||||
|
page =
|
||||||
|
Meeseeks.one(html, css("#content .pages.top option[selected]"))
|
||||||
|
|> Meeseeks.text()
|
||||||
|
|> case do
|
||||||
|
"" -> 1
|
||||||
|
s -> String.to_integer(s)
|
||||||
|
end
|
||||||
|
|
||||||
page_count =
|
page_count =
|
||||||
case Floki.find(html, "#content .pages.top option:last-of-type") |> Floki.text() do
|
Meeseeks.one(html, css("#content .pages.top option:last-of-type"))
|
||||||
|
|> Meeseeks.text()
|
||||||
|
|> case do
|
||||||
"" -> 1
|
"" -> 1
|
||||||
s -> String.to_integer(s)
|
s -> String.to_integer(s)
|
||||||
end
|
end
|
||||||
|
|
||||||
posts =
|
posts =
|
||||||
for post <- Floki.find(thread, "table.post") do
|
for post <- Meeseeks.all(thread, css("table.post:not(.ignored)")) do
|
||||||
%{
|
%{
|
||||||
userinfo: post |> userinfo(),
|
userinfo: userinfo(post),
|
||||||
postdate: post |> postdate(),
|
postdate: postdate(post),
|
||||||
postbody: post |> postbody()
|
postbody: postbody(post)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
%{id: id, title: title, page: page, page_count: page_count, posts: posts}
|
%{id: thread_id, title: title, page: page, page_count: page_count, posts: posts}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp userinfo(post) do
|
defp userinfo(post) do
|
||||||
user = Floki.find(post, "dl.userinfo")
|
user = Meeseeks.one(post, css("dl.userinfo"))
|
||||||
name = user |> Floki.find("dt") |> Floki.text()
|
name = user |> Meeseeks.one(css("dt")) |> Meeseeks.text()
|
||||||
regdate = user |> Floki.find("dd.registered") |> Floki.text()
|
regdate = user |> Meeseeks.one(css("dd.registered")) |> Meeseeks.text()
|
||||||
title = user |> Floki.find("dd.title") |> List.first() |> Floki.children() |> Floki.raw_html()
|
|
||||||
|
title =
|
||||||
|
user
|
||||||
|
|> Meeseeks.one(css("dd.title > *"))
|
||||||
|
|> Meeseeks.html()
|
||||||
|
|
||||||
%{
|
%{
|
||||||
name: name,
|
name: name,
|
||||||
@ -43,12 +64,15 @@ defmodule SomethingErlang.AwfulApi.Thread do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp postdate(post) do
|
defp postdate(post) do
|
||||||
date = Floki.find(post, "td.postdate") |> Floki.find("td.postdate") |> Floki.text()
|
date =
|
||||||
|
post
|
||||||
|
|> Meeseeks.one(css("td.postdate"))
|
||||||
|
|> Meeseeks.text()
|
||||||
|
|
||||||
[month_text, day, year, hours, minutes] =
|
[month_text, day, year, hours, minutes] =
|
||||||
date
|
date
|
||||||
|> String.split(~r{[\s,:]}, trim: true)
|
|> String.split(~r{[\s,:]}, trim: true)
|
||||||
|> Enum.drop(1)
|
|> Enum.drop(2)
|
||||||
|
|
||||||
month =
|
month =
|
||||||
1 +
|
1 +
|
||||||
@ -68,34 +92,35 @@ defmodule SomethingErlang.AwfulApi.Thread do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp postbody(post) do
|
defp postbody(post) do
|
||||||
body =
|
{_, _, body} =
|
||||||
Floki.find(post, "td.postbody")
|
post
|
||||||
|> List.first()
|
|> Meeseeks.one(css("td.postbody"))
|
||||||
|> Floki.filter_out(:comment)
|
|> Meeseeks.tree()
|
||||||
|
|
||||||
Floki.traverse_and_update(body, fn
|
body
|
||||||
{"img", attrs, []} -> transform(:img, attrs)
|
|> Enum.map(&transform/1)
|
||||||
{"a", attrs, children} -> transform(:a, attrs, children)
|
|> Enum.reject(fn x -> x == "" end)
|
||||||
other -> other
|
|> then(&{"div", [], &1})
|
||||||
end)
|
|> Meeseeks.parse(:tuple_tree)
|
||||||
|> Floki.children()
|
|> Meeseeks.html()
|
||||||
|> Floki.raw_html()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform(elem, attr, children \\ [])
|
defp transform({"img", attrs, _children}) do
|
||||||
|
|
||||||
defp transform(:img, attrs, _children) do
|
|
||||||
{"class", class} = List.keyfind(attrs, "class", 0, {"class", ""})
|
{"class", class} = List.keyfind(attrs, "class", 0, {"class", ""})
|
||||||
|
|
||||||
if class == "sa-smilie" do
|
if class == "sa-smilie" do
|
||||||
{"img", attrs, []}
|
{"img", attrs, []}
|
||||||
else
|
else
|
||||||
t_attrs = List.keyreplace(attrs, "class", 0, {"class", "img-responsive"})
|
t_attrs =
|
||||||
|
attrs
|
||||||
|
|> fix_attachment_links()
|
||||||
|
|> List.keyreplace("class", 0, {"class", "img-responsive"})
|
||||||
|
|
||||||
{"img", [{"loading", "lazy"} | t_attrs], []}
|
{"img", [{"loading", "lazy"} | t_attrs], []}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp transform(:a, attrs, children) do
|
defp transform({"a", attrs, children}) do
|
||||||
{"href", href} = List.keyfind(attrs, "href", 0, {"href", ""})
|
{"href", href} = List.keyfind(attrs, "href", 0, {"href", ""})
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
@ -124,6 +149,12 @@ defmodule SomethingErlang.AwfulApi.Thread do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp transform({:comment, _}), do: ""
|
||||||
|
defp transform({tag, attrs, children}), do: {tag, attrs, children}
|
||||||
|
|
||||||
|
defp transform(text) when is_binary(text),
|
||||||
|
do: String.trim(text)
|
||||||
|
|
||||||
defp transform_link(:mp4, href),
|
defp transform_link(:mp4, href),
|
||||||
do:
|
do:
|
||||||
{"div", [{"class", "responsive-embed"}],
|
{"div", [{"class", "responsive-embed"}],
|
||||||
@ -167,4 +198,15 @@ defmodule SomethingErlang.AwfulApi.Thread do
|
|||||||
{"src", src}
|
{"src", src}
|
||||||
], []}
|
], []}
|
||||||
]}
|
]}
|
||||||
|
|
||||||
|
defp fix_attachment_links(attrs) do
|
||||||
|
{"src", src} = List.keyfind(attrs, "src", 0)
|
||||||
|
|
||||||
|
if String.starts_with?(src, "attachment.php") do
|
||||||
|
List.keyreplace(attrs, "src", 0, {"src", "https://forums.somethingawful.com/" <> src})
|
||||||
|
|> List.keydelete("alt", 0)
|
||||||
|
else
|
||||||
|
attrs
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,48 +4,30 @@ defmodule SomethingErlang.Grover do
|
|||||||
alias SomethingErlang.AwfulApi
|
alias SomethingErlang.AwfulApi
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def mount(user) do
|
|
||||||
grover =
|
|
||||||
DynamicSupervisor.start_child(
|
|
||||||
SomethingErlang.Supervisor.Grovers,
|
|
||||||
{__MODULE__, [self(), user]}
|
|
||||||
)
|
|
||||||
|
|
||||||
case grover do
|
|
||||||
{:ok, pid} -> pid
|
|
||||||
{:error, {:already_started, pid}} -> pid
|
|
||||||
{:error, error} -> {:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_thread!(thread_id, page_number) do
|
def get_thread!(thread_id, page_number) do
|
||||||
GenServer.call(via(self()), {:show_thread, thread_id, page_number})
|
GenServer.call(__MODULE__, {:show_thread, thread_id, page_number})
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_bookmarks!(page_number) do
|
def get_bookmarks!(pid, page_number) do
|
||||||
GenServer.call(via(self()), {:show_bookmarks, page_number})
|
GenServer.call(pid, {:show_bookmarks, page_number})
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_link([lv_pid, user]) do
|
def start_link(user) do
|
||||||
GenServer.start_link(
|
GenServer.start_link(
|
||||||
__MODULE__,
|
__MODULE__,
|
||||||
[lv_pid, user],
|
[user]
|
||||||
name: via(lv_pid)
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init([pid, user]) do
|
def init([user]) do
|
||||||
%{bbuserid: userid, bbpassword: userhash} = user
|
%{bbuserid: userid, bbpassword: userhash} = user
|
||||||
|
|
||||||
initial_state = %{
|
initial_state = %{
|
||||||
lv_pid: pid,
|
|
||||||
user: %{id: userid, hash: userhash}
|
user: %{id: userid, hash: userhash}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.debug("init #{userid} #{inspect(pid)}")
|
{:ok, initial_state} |> dbg()
|
||||||
Process.monitor(pid)
|
|
||||||
{:ok, initial_state}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@ -55,22 +37,8 @@ defmodule SomethingErlang.Grover do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:show_bookmarks, _page_number}, _from, state) do
|
def handle_call({:show_bookmarks, page_number}, _from, state) do
|
||||||
bookmarks = AwfulApi.bookmarks(state.user)
|
bookmarks = AwfulApi.bookmarks(page_number, state.user)
|
||||||
{:reply, bookmarks, state}
|
{:reply, bookmarks, state} |> dbg()
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:DOWN, _ref, :process, _object, reason}, state) do
|
|
||||||
Logger.debug("received :DOWN from: #{inspect(state.lv_pid)} reason: #{inspect(reason)}")
|
|
||||||
|
|
||||||
case reason do
|
|
||||||
{:shutdown, _} -> {:stop, :normal, state}
|
|
||||||
:killed -> {:stop, :normal, state}
|
|
||||||
_ -> {:noreply, state}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp via(lv_pid),
|
|
||||||
do: {:via, Registry, {SomethingErlang.Registry.Grovers, lv_pid}}
|
|
||||||
end
|
|
||||||
|
54
lib/something_erlang_web/live/bookmarks_live.ex
Normal file
54
lib/something_erlang_web/live/bookmarks_live.ex
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
defmodule SomethingErlangWeb.BookmarksLive do
|
||||||
|
use SomethingErlangWeb, :live_view
|
||||||
|
|
||||||
|
alias SomethingErlang.Grover
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
assigns.bookmarks |> dbg()
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div :if={bookmarks = @bookmarks.ok? && @bookmarks.result}>
|
||||||
|
<.bookmark
|
||||||
|
:for={thread <- bookmarks}
|
||||||
|
id={thread.id}
|
||||||
|
title={thread.title.thread_title}
|
||||||
|
post_count={thread.replies}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp bookmark(assigns) do
|
||||||
|
assigns = assign(assigns, :pages, ceil(assigns.post_count / 40))
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<.link href={~p"/thread/#{@id}"}><%= @title %></.link>
|
||||||
|
<.link href={~p"/thread/#{@id}?page=#{@pages}"}><%= @pages %></.link>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def mount(_params, session, socket) do
|
||||||
|
user =
|
||||||
|
socket.assigns.current_user
|
||||||
|
|> Map.put(:bbpassword, session["bbpassword"])
|
||||||
|
|
||||||
|
{:ok, pid} = Grover.start_link(user) |> dbg()
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
socket
|
||||||
|
|> assign(:page_title, "Bookmarks")
|
||||||
|
|> assign_async(:bookmarks, fn ->
|
||||||
|
{:ok, bookmarks} = get_bookmarks(pid)
|
||||||
|
{:ok, %{bookmarks: bookmarks}}
|
||||||
|
end)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_bookmarks(pid) do
|
||||||
|
case Grover.get_bookmarks!(pid, 1) do
|
||||||
|
bookmarks when is_list(bookmarks) -> {:ok, bookmarks}
|
||||||
|
_ -> {:error, "Failed getting bookmarks"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -69,10 +69,10 @@ defmodule SomethingErlangWeb.ThreadLive do
|
|||||||
~H"""
|
~H"""
|
||||||
<div class="navbar my-4 bg-base-200">
|
<div class="navbar my-4 bg-base-200">
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
<div class="pagination flex-none btn-group grid grid-cols-5">
|
<div class="pagination join">
|
||||||
<%= for btn <- buttons(@thread) do %>
|
<%= for btn <- buttons(@thread) do %>
|
||||||
<.link
|
<.link
|
||||||
class={["btn btn-sm btn-ghost", btn.special]}
|
class={["btn btn-sm btn-ghost join-item", btn.special]}
|
||||||
navigate={~p"/thread/#{@thread.id}?page=#{btn.page}"}
|
navigate={~p"/thread/#{@thread.id}?page=#{btn.page}"}
|
||||||
>
|
>
|
||||||
<.label_button label={btn.label} page={btn.page} />
|
<.label_button label={btn.label} page={btn.page} />
|
||||||
@ -86,8 +86,8 @@ defmodule SomethingErlangWeb.ThreadLive do
|
|||||||
defp label_button(%{label: label} = assigns) do
|
defp label_button(%{label: label} = assigns) do
|
||||||
case label do
|
case label do
|
||||||
"«" -> ~H{<.icon name="hero-chevron-double-left-mini" /><%= @page %>}
|
"«" -> ~H{<.icon name="hero-chevron-double-left-mini" /><%= @page %>}
|
||||||
"‹" -> ~H{<.icon name="hero-chevron-left-mini" /><%= @page %>}
|
"‹" -> ~H{<.icon name="hero-chevron-left-mini" />}
|
||||||
"›" -> ~H{<%= @page %><.icon name="hero-chevron-right-mini" />}
|
"›" -> ~H{<.icon name="hero-chevron-right-mini" />}
|
||||||
"»" -> ~H{<%= @page %><.icon name="hero-chevron-double-right-mini" />}
|
"»" -> ~H{<%= @page %><.icon name="hero-chevron-double-right-mini" />}
|
||||||
_ -> ~H{<%= @page %>}
|
_ -> ~H{<%= @page %>}
|
||||||
end
|
end
|
||||||
|
@ -28,6 +28,8 @@ defmodule SomethingErlangWeb.Router do
|
|||||||
on_mount: [{SomethingErlangWeb.UserAuth, :ensure_authenticated}] do
|
on_mount: [{SomethingErlangWeb.UserAuth, :ensure_authenticated}] do
|
||||||
live("/thread", ThreadLive)
|
live("/thread", ThreadLive)
|
||||||
live("/thread/:id", ThreadLive)
|
live("/thread/:id", ThreadLive)
|
||||||
|
live("/bookmarks", BookmarksLive)
|
||||||
|
live("/bookmarks/:id", BookmarksLive)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ defmodule SomethingErlangWeb.UserAuth do
|
|||||||
@max_age 60 * 60 * 24 * 60
|
@max_age 60 * 60 * 24 * 60
|
||||||
@remember_me_cookie "_something_erlang_web_user_remember_me"
|
@remember_me_cookie "_something_erlang_web_user_remember_me"
|
||||||
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
|
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]
|
||||||
|
@bbpassword_options [sign: false, max_age: @max_age, same_site: "Lax"]
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Logs the user in.
|
Logs the user in.
|
||||||
@ -38,7 +39,7 @@ defmodule SomethingErlangWeb.UserAuth do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp put_hashcookie_in_session(conn, bbpassword) do
|
defp put_hashcookie_in_session(conn, bbpassword) do
|
||||||
put_resp_cookie(conn, "bbpassword", bbpassword)
|
put_resp_cookie(conn, "bbpassword", bbpassword, @bbpassword_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
|
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
|
||||||
@ -185,7 +186,7 @@ defmodule SomethingErlangWeb.UserAuth do
|
|||||||
|
|
||||||
defp mount_current_user(session, socket) do
|
defp mount_current_user(session, socket) do
|
||||||
case session do
|
case session do
|
||||||
%{"user_token" => user_token} ->
|
%{"user_token" => user_token, "bbpassword" => bbpw} when not is_nil(bbpw) ->
|
||||||
Phoenix.Component.assign_new(socket, :current_user, fn ->
|
Phoenix.Component.assign_new(socket, :current_user, fn ->
|
||||||
Accounts.get_user_by_session_token(user_token)
|
Accounts.get_user_by_session_token(user_token)
|
||||||
end)
|
end)
|
||||||
|
7
mix.exs
7
mix.exs
@ -39,8 +39,7 @@ defmodule SomethingErlang.MixProject do
|
|||||||
{:phoenix_html, "~> 4.0"},
|
{:phoenix_html, "~> 4.0"},
|
||||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||||
{:phoenix_live_view, "~> 0.20.2"},
|
{:phoenix_live_view, "~> 0.20.2"},
|
||||||
# {:floki, ">= 0.30.0", only: :test},
|
{:floki, ">= 0.30.0", only: :test},
|
||||||
{:floki, ">= 0.30.0"},
|
|
||||||
{:phoenix_live_dashboard, "~> 0.8.3"},
|
{:phoenix_live_dashboard, "~> 0.8.3"},
|
||||||
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
|
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
|
||||||
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
|
{:tailwind, "~> 0.2", runtime: Mix.env() == :dev},
|
||||||
@ -59,7 +58,9 @@ defmodule SomethingErlang.MixProject do
|
|||||||
{:jason, "~> 1.2"},
|
{:jason, "~> 1.2"},
|
||||||
{:dns_cluster, "~> 0.1.1"},
|
{:dns_cluster, "~> 0.1.1"},
|
||||||
{:bandit, "~> 1.2"},
|
{:bandit, "~> 1.2"},
|
||||||
{:req, "~> 0.5.0"}
|
{:meeseeks, "~> 0.17.0"},
|
||||||
|
{:req, "~> 0.5.0"},
|
||||||
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
5
mix.lock
5
mix.lock
@ -1,6 +1,8 @@
|
|||||||
%{
|
%{
|
||||||
"bandit": {:hex, :bandit, "1.5.2", "ed0a41c43a9e529c670d0fd48371db4027e7b80d43b1942893e17deb8bed0540", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "35ddbdce7e8a2a3c6b5093f7299d70832a43ed2f4a1852885a61d334cab1b4ad"},
|
"bandit": {:hex, :bandit, "1.5.2", "ed0a41c43a9e529c670d0fd48371db4027e7b80d43b1942893e17deb8bed0540", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "35ddbdce7e8a2a3c6b5093f7299d70832a43ed2f4a1852885a61d334cab1b4ad"},
|
||||||
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"},
|
||||||
|
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [: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", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||||
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
|
||||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||||
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
|
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
|
||||||
@ -15,6 +17,8 @@
|
|||||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
|
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]},
|
||||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||||
|
"meeseeks": {:hex, :meeseeks, "0.17.0", "8a41ceccd2365476c2b779292e7649fb25f0a9735030905941f1244d2095c8a6", [:mix], [{:meeseeks_html5ever, "~> 0.14.3", [hex: :meeseeks_html5ever, repo: "hexpm", optional: false]}], "hexpm", "13efaf321a1517dea046cb48ff9baa9dc0604d9afd82c57501bc01dc45a5e309"},
|
||||||
|
"meeseeks_html5ever": {:hex, :meeseeks_html5ever, "0.14.3", "7827c6ce393d9f99dd0220c356fd66ee7101718037ec6f7f18d4bcba84ef1798", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "6b69573b97120fcc6e97045178ad085fd3ee10a5b49c1e9ebb8a28bd4a9c538b"},
|
||||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||||
"mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"},
|
"mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||||
@ -31,6 +35,7 @@
|
|||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||||
"postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [: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", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"},
|
"postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [: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", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"},
|
||||||
"req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"},
|
"req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"},
|
||||||
|
"rustler_precompiled": {:hex, :rustler_precompiled, "0.6.3", "f838d94bc35e1844973ee7266127b156fdc962e9e8b7ff666c8fb4fed7964d23", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e18ecca3669a7454b3a2be75ae6c3ef01d550bc9a8cf5fbddcfff843b881d7c6"},
|
||||||
"swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
|
"swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
|
||||||
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
"tailwind": {:hex, :tailwind, "0.2.2", "9e27288b568ede1d88517e8c61259bc214a12d7eed271e102db4c93fcca9b2cd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "ccfb5025179ea307f7f899d1bb3905cd0ac9f687ed77feebc8f67bdca78565c4"},
|
||||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
|
Reference in New Issue
Block a user