Initial commit
This commit is contained in:
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
# flyctl launch added from .gitignore
|
||||
**/.env
|
||||
**/public/key.pub
|
||||
**/.idea/*
|
||||
**/log/*
|
||||
|
||||
# flyctl launch added from .idea/.gitignore
|
||||
# Default ignored files
|
||||
.idea/shelf
|
||||
.idea/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
.idea/httpRequests
|
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
.env
|
||||
public/key.pub
|
||||
public/media/**/*
|
||||
.idea/*
|
||||
log/*
|
||||
fly.toml
|
||||
*.sock
|
||||
publish.rb
|
||||
publish_bookmark.rb
|
||||
node_modules/*
|
||||
.DS_Store
|
||||
.bundle/
|
||||
.env.*
|
||||
.vimrc.local
|
||||
|
2
.tool-versions
Normal file
2
.tool-versions
Normal file
@@ -0,0 +1,2 @@
|
||||
ruby 3.2.0
|
||||
postgres 15.1
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM ruby:3.2-alpine
|
||||
|
||||
RUN apk update && apk add build-base git sqlite-dev postgresql-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY Gemfile* ./
|
||||
|
||||
RUN gem install bundler:2.2.16 && bundle install --jobs 4 --retry 5
|
||||
|
||||
COPY . .
|
51
Gemfile
Normal file
51
Gemfile
Normal file
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "hanami", "~> 2.0.0"
|
||||
gem "hanami-router", "~> 2.0.0"
|
||||
gem "hanami-controller", "~> 2.0.0"
|
||||
gem "hanami-validations", "~> 2.0.0"
|
||||
# gem "hanami-assets", github: "hanami/view", branch: "main"
|
||||
gem "hanami-view", github: "hanami/view", branch: "main"
|
||||
|
||||
gem "rom-sql"
|
||||
gem "pg"
|
||||
|
||||
gem "dry-types"
|
||||
gem "dry-matcher"
|
||||
gem "dry-monads"
|
||||
gem "puma"
|
||||
gem "rake"
|
||||
gem "slim"
|
||||
gem "builder"
|
||||
|
||||
gem "httparty"
|
||||
gem "redcarpet"
|
||||
gem "rexml"
|
||||
gem "babosa"
|
||||
|
||||
group :development, :test do
|
||||
gem "dotenv"
|
||||
end
|
||||
|
||||
group :cli, :development do
|
||||
gem "hanami-reloader"
|
||||
end
|
||||
|
||||
group :cli, :development, :test do
|
||||
gem "hanami-rspec"
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "guard-puma", "~> 0.8"
|
||||
gem "standardrb"
|
||||
gem "capistrano", "~> 3.17", require: false
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem "rack-test"
|
||||
gem "rom-factory"
|
||||
gem "database_cleaner-sequel"
|
||||
gem "timecop"
|
||||
end
|
326
Gemfile.lock
Normal file
326
Gemfile.lock
Normal file
@@ -0,0 +1,326 @@
|
||||
GIT
|
||||
remote: https://github.com/hanami/view.git
|
||||
revision: c1a6f60a989f1face809a6d8d61652748aee19a7
|
||||
branch: main
|
||||
specs:
|
||||
hanami-view (2.0.0.alpha8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0, < 2)
|
||||
tilt (~> 2.0, >= 2.0.6)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
airbrussh (1.4.1)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
ast (2.4.2)
|
||||
babosa (2.0.0)
|
||||
builder (3.2.4)
|
||||
capistrano (3.17.1)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.1.10)
|
||||
database_cleaner-core (2.0.1)
|
||||
database_cleaner-sequel (2.0.2)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
sequel
|
||||
diff-lcs (1.5.0)
|
||||
dotenv (2.8.1)
|
||||
dry-auto_inject (1.0.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-cli (1.0.0)
|
||||
dry-configurable (1.0.1)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-core (1.0.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-events (1.0.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-files (1.0.1)
|
||||
dry-inflector (1.0.0)
|
||||
dry-initializer (3.1.1)
|
||||
dry-logger (1.0.3)
|
||||
dry-logic (1.5.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-matcher (1.0.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-monads (1.6.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-monitor (1.0.1)
|
||||
dry-configurable (~> 1.0, < 2)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-events (~> 1.0, < 2)
|
||||
dry-schema (1.13.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-configurable (~> 1.0, >= 1.0.1)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-initializer (~> 3.0)
|
||||
dry-logic (>= 1.5, < 2)
|
||||
dry-types (>= 1.7, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-struct (1.6.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-types (>= 1.7, < 2)
|
||||
ice_nine (~> 0.11)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-system (1.0.1)
|
||||
dry-auto_inject (~> 1.0, < 2)
|
||||
dry-configurable (~> 1.0, < 2)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-inflector (~> 1.0, < 2)
|
||||
dry-transformer (1.0.1)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-types (1.7.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-inflector (~> 1.0, < 2)
|
||||
dry-logic (>= 1.4, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
dry-validation (1.10.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-initializer (~> 3.0)
|
||||
dry-schema (>= 1.12, < 2)
|
||||
zeitwerk (~> 2.6)
|
||||
faker (2.23.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
ffi (1.15.5)
|
||||
formatador (1.1.0)
|
||||
guard (2.18.0)
|
||||
formatador (>= 0.2.4)
|
||||
listen (>= 2.7, < 4.0)
|
||||
lumberjack (>= 1.0.12, < 2.0)
|
||||
nenv (~> 0.1)
|
||||
notiffany (~> 0.0)
|
||||
pry (>= 0.13.0)
|
||||
shellany (~> 0.0)
|
||||
thor (>= 0.18.1)
|
||||
guard-compat (1.2.1)
|
||||
guard-puma (0.8.0)
|
||||
guard (~> 2.14)
|
||||
guard-compat (~> 1.2)
|
||||
puma (>= 4.0, < 7)
|
||||
hanami (2.0.2)
|
||||
bundler (>= 1.16, < 3)
|
||||
dry-configurable (~> 1.0, < 2)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-inflector (~> 1.0, < 2)
|
||||
dry-logger (~> 1.0, < 2)
|
||||
dry-monitor (~> 1.0, >= 1.0.1, < 2)
|
||||
dry-system (~> 1.0, < 2)
|
||||
hanami-cli (~> 2.0)
|
||||
hanami-utils (~> 2.0)
|
||||
zeitwerk (~> 2.6)
|
||||
hanami-cli (2.0.2)
|
||||
bundler (~> 2.1)
|
||||
dry-cli (~> 1.0, < 2)
|
||||
dry-files (~> 1.0, >= 1.0.1, < 2)
|
||||
dry-inflector (~> 1.0, < 2)
|
||||
rake (~> 13.0)
|
||||
zeitwerk (~> 2.6)
|
||||
hanami-controller (2.0.1)
|
||||
dry-configurable (~> 1.0, < 2)
|
||||
dry-core (~> 1.0)
|
||||
hanami-utils (~> 2.0)
|
||||
rack (~> 2.0)
|
||||
zeitwerk (~> 2.6)
|
||||
hanami-reloader (2.0.2)
|
||||
hanami-cli (~> 2.0)
|
||||
zeitwerk (~> 2.6)
|
||||
hanami-router (2.0.2)
|
||||
mustermann (~> 3.0)
|
||||
mustermann-contrib (~> 3.0)
|
||||
rack (~> 2.0)
|
||||
hanami-rspec (2.0.1)
|
||||
hanami-cli (~> 2.0)
|
||||
rake (~> 13.0)
|
||||
rspec (~> 3.12)
|
||||
zeitwerk (~> 2.6)
|
||||
hanami-utils (2.0.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
dry-core (~> 1.0, < 2)
|
||||
dry-transformer (~> 1.0, < 2)
|
||||
hanami-validations (2.0.1)
|
||||
dry-validation (>= 1.10, < 2)
|
||||
zeitwerk (~> 2.6.0)
|
||||
hansi (0.2.1)
|
||||
httparty (0.21.0)
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
json (2.6.3)
|
||||
language_server-protocol (3.17.0.2)
|
||||
listen (3.7.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
lumberjack (1.2.8)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.1.2)
|
||||
multi_xml (0.6.0)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
mustermann-contrib (3.0.0)
|
||||
hansi (~> 0.2.0)
|
||||
mustermann (= 3.0.0)
|
||||
nenv (0.3.0)
|
||||
net-scp (4.0.0)
|
||||
net-ssh (>= 2.6.5, < 8.0.0)
|
||||
net-ssh (7.0.1)
|
||||
nio4r (2.5.8)
|
||||
notiffany (0.1.3)
|
||||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
parallel (1.22.1)
|
||||
parser (3.2.0.0)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.4.5)
|
||||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
puma (6.0.1)
|
||||
nio4r (~> 2.0)
|
||||
rack (2.2.5)
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
redcarpet (3.5.1)
|
||||
regexp_parser (2.6.1)
|
||||
rexml (3.2.5)
|
||||
rom (5.3.0)
|
||||
rom-changeset (~> 5.3, >= 5.3.0)
|
||||
rom-core (~> 5.3, >= 5.3.0)
|
||||
rom-repository (~> 5.3, >= 5.3.0)
|
||||
rom-changeset (5.3.0)
|
||||
dry-core (~> 1.0)
|
||||
rom-core (~> 5.3)
|
||||
transproc (~> 1.0, >= 1.1.0)
|
||||
rom-core (5.3.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
dry-configurable (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-inflector (~> 1.0)
|
||||
dry-initializer (~> 3.0, >= 3.0.1)
|
||||
dry-struct (~> 1.0)
|
||||
dry-types (~> 1.6)
|
||||
transproc (~> 1.0, >= 1.1.0)
|
||||
rom-factory (0.11.0)
|
||||
dry-configurable (~> 1.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-struct (~> 1.6)
|
||||
faker (>= 2.0, < 3.0)
|
||||
rom-core (~> 5.3)
|
||||
rom-repository (5.3.0)
|
||||
dry-core (~> 1.0)
|
||||
dry-initializer (~> 3.0, >= 3.0.1)
|
||||
rom-core (~> 5.3, >= 5.3.0)
|
||||
rom-sql (3.6.1)
|
||||
dry-core (~> 1.0)
|
||||
dry-types (~> 1.0)
|
||||
rom (~> 5.2, >= 5.2.1)
|
||||
sequel (>= 4.49)
|
||||
rspec (3.12.0)
|
||||
rspec-core (~> 3.12.0)
|
||||
rspec-expectations (~> 3.12.0)
|
||||
rspec-mocks (~> 3.12.0)
|
||||
rspec-core (3.12.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-support (3.12.0)
|
||||
rubocop (1.42.0)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.2.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.24.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.24.1)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-performance (1.15.2)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
sequel (5.63.0)
|
||||
shellany (0.0.1)
|
||||
slim (4.1.0)
|
||||
temple (>= 0.7.6, < 0.9)
|
||||
tilt (>= 2.0.6, < 2.1)
|
||||
sshkit (1.21.3)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
standard (1.21.1)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
rubocop (= 1.42.0)
|
||||
rubocop-performance (= 1.15.2)
|
||||
standardrb (1.0.1)
|
||||
standard
|
||||
temple (0.8.2)
|
||||
thor (1.2.1)
|
||||
tilt (2.0.11)
|
||||
timecop (0.9.6)
|
||||
transproc (1.1.1)
|
||||
unicode-display_width (2.4.2)
|
||||
zeitwerk (2.6.6)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-22
|
||||
|
||||
DEPENDENCIES
|
||||
babosa
|
||||
builder
|
||||
capistrano (~> 3.17)
|
||||
database_cleaner-sequel
|
||||
dotenv
|
||||
dry-matcher
|
||||
dry-monads
|
||||
dry-types
|
||||
guard-puma (~> 0.8)
|
||||
hanami (~> 2.0.0)
|
||||
hanami-controller (~> 2.0.0)
|
||||
hanami-reloader
|
||||
hanami-router (~> 2.0.0)
|
||||
hanami-rspec
|
||||
hanami-validations (~> 2.0.0)
|
||||
hanami-view!
|
||||
httparty
|
||||
pg
|
||||
puma
|
||||
rack-test
|
||||
rake
|
||||
redcarpet
|
||||
rexml
|
||||
rom-factory
|
||||
rom-sql
|
||||
slim
|
||||
standardrb
|
||||
timecop
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.22
|
10
Guardfile
Normal file
10
Guardfile
Normal file
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
group :server do
|
||||
guard "puma", port: ENV.fetch("HANAMI_PORT", 2300) do
|
||||
watch(%r{config/*})
|
||||
watch(%r{lib/*})
|
||||
watch(%r{app/*})
|
||||
watch(%r{slices/*})
|
||||
end
|
||||
end
|
2
Procfile.dev
Normal file
2
Procfile.dev
Normal file
@@ -0,0 +1,2 @@
|
||||
web: bundle exec hanami server
|
||||
css: npx tailwindcss -i ./app/assets/index.css -o ./public/assets/index.css --watch
|
3
Rakefile
Normal file
3
Rakefile
Normal file
@@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "hanami/rake_tasks"
|
62
app/action.rb
Normal file
62
app/action.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# auto_register: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "hanami/action"
|
||||
require "httparty"
|
||||
|
||||
module Adamantium
|
||||
class Action < Hanami::Action
|
||||
include Deps["logger", "settings", not_found_view: "views.not_found"]
|
||||
|
||||
handle_exception ROM::TupleCountMismatchError => :not_found
|
||||
|
||||
def authenticate!(req, res)
|
||||
if Hanami.env == :development || Hanami.env == :test
|
||||
req.env[:scopes] = verify_token(nil)
|
||||
return true
|
||||
end
|
||||
|
||||
# Pull out and verify the authorization header or access_token
|
||||
if req.env["HTTP_AUTHORIZATION"]
|
||||
header = req.env["HTTP_AUTHORIZATION"].match(/Bearer (.*)$/)
|
||||
access_token = header[1] unless header.nil?
|
||||
elsif req.params["access_token"]
|
||||
access_token = req.params["access_token"]
|
||||
else
|
||||
logger.error "Received request without a token"
|
||||
halt 401
|
||||
end
|
||||
|
||||
req.env[:access_token] = access_token
|
||||
|
||||
# Verify the token and extract scopes
|
||||
req.env[:scopes] = verify_token(access_token)
|
||||
end
|
||||
|
||||
def not_found(_req, res, _exception)
|
||||
res.render not_found_view
|
||||
end
|
||||
|
||||
def verify_scope(req:, scope:)
|
||||
req.env[:scopes].include? scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_token(access_token)
|
||||
return %i[create update delete undelete media] if Hanami.env == :development || Hanami.env == :test
|
||||
|
||||
resp = HTTParty.get(settings.micropub_token_endpoint, {
|
||||
headers: {
|
||||
"Accept" => "application/x-www-form-urlencoded",
|
||||
"Authorization" => "Bearer #{access_token}"
|
||||
}
|
||||
})
|
||||
decoded_response = URI.decode_www_form(resp.body).to_h.transform_keys(&:to_sym)
|
||||
|
||||
halt 401 unless (decoded_response.include? :scope) && (decoded_response.include? :me)
|
||||
|
||||
decoded_response[:scope].gsub(/post/, "create").split.map(&:to_sym)
|
||||
end
|
||||
end
|
||||
end
|
0
app/actions/.keep
Normal file
0
app/actions/.keep
Normal file
13
app/actions/bookmarks/index.rb
Normal file
13
app/actions/bookmarks/index.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Bookmarks
|
||||
class Index < Action
|
||||
include Deps["views.bookmarks.index"]
|
||||
|
||||
def handle(req, res)
|
||||
res.render index, query: req.params[:q]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
app/actions/bookmarks/show.rb
Normal file
12
app/actions/bookmarks/show.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Bookmarks
|
||||
class Show < Action
|
||||
include Deps["views.bookmarks.show"]
|
||||
def handle(req, res)
|
||||
res.render show, slug: req.params[:slug]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
app/actions/feeds/rss.rb
Normal file
14
app/actions/feeds/rss.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Feeds
|
||||
class Rss < Action
|
||||
include Deps["views.feeds.rss"]
|
||||
|
||||
def handle(req, res)
|
||||
res.content_type = "application/rss+xml"
|
||||
res.render rss, format: :xml
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/actions/key/show.rb
Normal file
13
app/actions/key/show.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Key
|
||||
class Show < Action
|
||||
include Deps["settings"]
|
||||
def handle(req, res)
|
||||
res.content_type = "text/plain"
|
||||
res.body = settings.micropub_pub_key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
28
app/actions/media/create.rb
Normal file
28
app/actions/media/create.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Media
|
||||
class Create < Action
|
||||
before :authenticate
|
||||
|
||||
include Deps["commands.media.upload"]
|
||||
|
||||
def handle(req, res)
|
||||
data = req.params[:file]
|
||||
|
||||
halt 401 if verify_scope(req: req, scope: :media)
|
||||
|
||||
upload(file: data) do |m|
|
||||
m.failure do |v|
|
||||
res.status = 422
|
||||
end
|
||||
|
||||
m.success do |v|
|
||||
res.status = 201
|
||||
res.body = v
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
app/actions/pages/show.rb
Normal file
15
app/actions/pages/show.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Pages
|
||||
class Show < Action
|
||||
include Deps["views.pages.show"]
|
||||
|
||||
def handle(req, res)
|
||||
slug = req.params[:slug]
|
||||
|
||||
res.render show, slug: slug
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
66
app/actions/posts/handle.rb
Normal file
66
app/actions/posts/handle.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
require "pry"
|
||||
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Posts
|
||||
class Handle < Action
|
||||
before :authenticate!
|
||||
|
||||
include Deps[
|
||||
"settings",
|
||||
post_param_parser: "param_parser.micropub_post",
|
||||
create_resolver: "commands.posts.creation_resolver",
|
||||
delete_post: "commands.posts.delete",
|
||||
undelete_post: "commands.posts.undelete",
|
||||
update_post: "commands.posts.update"
|
||||
]
|
||||
|
||||
def handle(req, res)
|
||||
req_entity = post_param_parser.call(params: req.params.to_h)
|
||||
action = req.params[:action]
|
||||
|
||||
if action
|
||||
operation, permission_check = resolve_operation(action)
|
||||
|
||||
if permission_check.call(req)
|
||||
operation.call(params: req.params.to_h)
|
||||
res.status = 200
|
||||
else
|
||||
res.status = 401
|
||||
end
|
||||
elsif req_entity
|
||||
halt 401 unless verify_scope(req: req, scope: :create)
|
||||
|
||||
command, contract = create_resolver.call(entry_type: req_entity).values_at(:command, :validation)
|
||||
|
||||
validation = contract.call(req_entity.to_h)
|
||||
if validation.success?
|
||||
post = command.call(validation.to_h)
|
||||
|
||||
res.status = 201
|
||||
res.headers.merge!(
|
||||
"Location" => "#{settings.micropub_site_url}/#{post.post_type}/#{post.slug}"
|
||||
)
|
||||
else
|
||||
res.body = {error: validation.errors.to_h}.to_json
|
||||
res.status = 422
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolve_operation(action)
|
||||
case action
|
||||
when "delete"
|
||||
[delete_post, ->(req) { verify_scope(req: req, scope: :delete) }]
|
||||
when "undelete"
|
||||
[undelete_post, ->(req) { verify_scope(req: req, scope: :undelete) }]
|
||||
when "update"
|
||||
[update_post, ->(req) { verify_scope(req: req, scope: :update) }]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
app/actions/posts/index.rb
Normal file
12
app/actions/posts/index.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Posts
|
||||
class Index < Action
|
||||
include Deps["views.posts.index"]
|
||||
def handle(req, res)
|
||||
res.render index
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
app/actions/posts/show.rb
Normal file
15
app/actions/posts/show.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Posts
|
||||
class Show < Action
|
||||
include Deps["views.posts.show"]
|
||||
|
||||
def handle(req, res)
|
||||
slug = req.params[:slug]
|
||||
|
||||
res.render show, slug: slug
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
31
app/actions/site/config.rb
Normal file
31
app/actions/site/config.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Site
|
||||
class Config < Action
|
||||
include Deps["settings", "views.site.home"]
|
||||
|
||||
def handle(req, res)
|
||||
if req.params[:q] == "config"
|
||||
res.status = 200
|
||||
res.content_type = "Application/JSON"
|
||||
res.body = {
|
||||
"media-endpoint" => settings.micropub_media_endpoint,
|
||||
"destination" => [
|
||||
{uid: settings.micropub_site_id, name: settings.micropub_site_name}
|
||||
],
|
||||
"post-types" => [
|
||||
{type: "note", name: "Note", properties: %w[content category]},
|
||||
{type: "article", name: "Article", properties: %w[name content category]},
|
||||
{type: "photo", name: "Photo", properties: %w[name content category]},
|
||||
{type: "bookmark", name: "Bookmark", properties: %w[name content category]}
|
||||
],
|
||||
"syndicate-to" => []
|
||||
}.to_json
|
||||
else
|
||||
res.render home
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
app/actions/site/home.rb
Normal file
12
app/actions/site/home.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Site
|
||||
class Home < Action
|
||||
include Deps["views.site.home"]
|
||||
def handle(req, res)
|
||||
res.render home
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
app/actions/tags/show.rb
Normal file
15
app/actions/tags/show.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Adamantium
|
||||
module Actions
|
||||
module Tags
|
||||
class Show < Action
|
||||
include Deps["views.tags.show"]
|
||||
|
||||
def handle(req, res)
|
||||
slug = req.params[:slug]
|
||||
|
||||
res.render show, slug: slug
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
27
app/assets/index.css
Normal file
27
app/assets/index.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind typography;
|
||||
|
||||
@font-face {
|
||||
font-family: "Rubik";
|
||||
src: url("/assets/Rubik-VariableFont_wght.ttf") format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "JetBrainsMono";
|
||||
src: url("/assets/JetBrainsMono-VariableFont_wght.ttf") format("truetype");
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Rubik", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.gist tr {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gist span {
|
||||
font-family: "JetBrainsMono", Monaco, monospace;
|
||||
}
|
||||
|
32
app/commands/media/upload.rb
Normal file
32
app/commands/media/upload.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Media
|
||||
class Upload < Command
|
||||
include Deps["settings"]
|
||||
|
||||
def call(file:)
|
||||
pathname = Time.now.strftime("%m-%Y")
|
||||
|
||||
filename = file[:filename].split("/").last
|
||||
|
||||
dirname = File.join("public", "media", pathname)
|
||||
|
||||
unless File.directory?(dirname)
|
||||
FileUtils.mkdir_p(dirname)
|
||||
end
|
||||
|
||||
begin
|
||||
File.write(File.join(dirname, filename), file[:tempfile].read)
|
||||
rescue Errno::ENOENT, NoMethodError => e
|
||||
return Failure(e.message)
|
||||
end
|
||||
|
||||
upload_path = File.join(settings.micropub_site_url, "/media/", "/#{pathname}/", filename).to_s
|
||||
Success(upload_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
12
app/commands/posts/create_bookmark.rb
Normal file
12
app/commands/posts/create_bookmark.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Posts
|
||||
class CreateBookmark < Command
|
||||
include Deps["repos.post_repo"]
|
||||
def call(bookmark)
|
||||
post_repo.create(bookmark)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
18
app/commands/posts/create_entry.rb
Normal file
18
app/commands/posts/create_entry.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Posts
|
||||
class CreateEntry < Command
|
||||
include Deps["repos.post_repo",
|
||||
"post_utilities.slugify",
|
||||
renderer: "renderers.markdown"
|
||||
]
|
||||
def call(post)
|
||||
attrs = post.to_h
|
||||
attrs[:content] = renderer.call(content: attrs[:content])
|
||||
|
||||
post_repo.create(attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
app/commands/posts/creation_resolver.rb
Normal file
23
app/commands/posts/creation_resolver.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Posts
|
||||
class CreationResolver
|
||||
include Deps[
|
||||
"validation.posts.post_contract",
|
||||
"validation.posts.bookmark_contract",
|
||||
"commands.posts.create_entry",
|
||||
"commands.posts.create_bookmark"
|
||||
]
|
||||
|
||||
def call(entry_type:)
|
||||
case entry_type
|
||||
in Entities::BookmarkRequest
|
||||
{command: create_bookmark, validation: bookmark_contract}
|
||||
else
|
||||
{command: create_entry, validation: post_contract}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
app/commands/posts/delete.rb
Normal file
14
app/commands/posts/delete.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Posts
|
||||
class Delete < Command
|
||||
include Deps["repos.post_repo"]
|
||||
def call(params:)
|
||||
slug = URI(params[:url]).path.split("/").last
|
||||
|
||||
post_repo.delete!(slug)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
app/commands/posts/undelete.rb
Normal file
14
app/commands/posts/undelete.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Posts
|
||||
class Undelete < Command
|
||||
include Deps["repos.post_repo"]
|
||||
def call(params:)
|
||||
slug = URI(params[:url]).path.split("/").last
|
||||
|
||||
post_repo.restore!(slug)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/commands/posts/update.rb
Normal file
13
app/commands/posts/update.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Adamantium
|
||||
module Commands
|
||||
module Posts
|
||||
class Update < Command
|
||||
def call(params)
|
||||
slug = URI(params[:url]).path.split("/").last
|
||||
|
||||
post_repo.update(slug, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
2
app/content/home.md
Normal file
2
app/content/home.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Hi! 👋 I'm Daniel, a software engineer living in Canberra, Australia.
|
||||
|
17
app/content/pages/about.md
Normal file
17
app/content/pages/about.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# About
|
||||
|
||||
Hi, I'm _Daniel_.
|
||||
|
||||
I've been in the software / web industry for around 10 years. I'm currently a technical lead at [Culture Amp](https://cultureamp.com), where I lead a small team working in a Rails monolith and across many microservices. Previously I worked with the wonderful humans of [Icelab](https://icelab.com.au) on a wide range of interesting and valuable projects.
|
||||
|
||||
I currently live in Canberra with my partner and [our dogs](https://instagram.com/barkly_and_crumpet).
|
||||
|
||||
In my spare time I like to tinker on various Ruby projects (including the software that powers this blog), make things with [Processing](https://processing.org), explore the various walking tracks around our house and potter around in the garden.
|
||||
|
||||
### Contact me
|
||||
|
||||
- [Email](mailto:hello@dnitza.com)
|
||||
- [Mastodon](https://social.dnitza.com/@daniel)
|
||||
- [Github](https://github.com/dnitza)
|
||||
|
||||
|
1
app/content/pages/art-things.md
Normal file
1
app/content/pages/art-things.md
Normal file
@@ -0,0 +1 @@
|
||||
# Art things
|
15
app/decorators/bookmarks/decorator.rb
Normal file
15
app/decorators/bookmarks/decorator.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# auto_register: false
|
||||
|
||||
module Adamantium
|
||||
module Decorators
|
||||
module Bookmarks
|
||||
class Decorator < SimpleDelegator
|
||||
def display_published_at
|
||||
published_at.strftime("%e %B, %Y")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
73
app/decorators/posts/decorator.rb
Normal file
73
app/decorators/posts/decorator.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
# frozen_string_literal: false
|
||||
|
||||
# auto_register: false
|
||||
|
||||
require "rexml/parsers/pullparser"
|
||||
|
||||
module Adamantium
|
||||
module Decorators
|
||||
module Posts
|
||||
class Decorator < SimpleDelegator
|
||||
def prefix_emoji
|
||||
name ? "📝" : "📯"
|
||||
end
|
||||
|
||||
def display_title
|
||||
title = name || published_at.strftime("%D")
|
||||
"#{prefix_emoji} #{title}"
|
||||
end
|
||||
|
||||
def display_published_at
|
||||
published_at.strftime("%e %B, %Y")
|
||||
end
|
||||
|
||||
def machine_published_at
|
||||
published_at.rfc2822
|
||||
end
|
||||
|
||||
def excerpt
|
||||
truncate_html(content, 140, true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def truncate_html(content, len = 30, at_end = nil)
|
||||
p = REXML::Parsers::PullParser.new(content)
|
||||
tags = []
|
||||
new_len = len
|
||||
results = ""
|
||||
while p.has_next? && new_len > 0
|
||||
p_e = p.pull
|
||||
case p_e.event_type
|
||||
when :start_element
|
||||
tags.push p_e[0]
|
||||
results << "<#{tags.last}#{attrs_to_s(p_e[1])}>"
|
||||
when :end_element
|
||||
results << "</#{tags.pop}>"
|
||||
when :text
|
||||
results << p_e[0][0..new_len]
|
||||
new_len -= p_e[0].length
|
||||
else
|
||||
results << "<!-- #{p_e.inspect} -->"
|
||||
end
|
||||
end
|
||||
if at_end
|
||||
results << "..."
|
||||
end
|
||||
tags.reverse_each do |tag|
|
||||
results << "</#{tag}>"
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
def attrs_to_s(attrs)
|
||||
if attrs.empty?
|
||||
""
|
||||
else
|
||||
" " + attrs.to_a.map { |attr| %(#{attr[0]}="#{attr[1]}") }.join(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
app/entities/bookmark_request.rb
Normal file
15
app/entities/bookmark_request.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Adamantium
|
||||
module Entities
|
||||
class BookmarkRequest < Dry::Struct
|
||||
attribute :h, Types::Coercible::String
|
||||
attribute :action, Types::Coercible::String.optional
|
||||
attribute :name, Types::Coercible::String
|
||||
attribute :content, Types::Coercible::String.optional
|
||||
attribute :url, Types::Coercible::String
|
||||
attribute :slug, Types::Coercible::String
|
||||
attribute :category, Types::Array.of(Types::Coercible::String)
|
||||
attribute :published_at, Types::Nominal::DateTime.optional
|
||||
attribute :post_type, Types::Coercible::String
|
||||
end
|
||||
end
|
||||
end
|
14
app/entities/post_request.rb
Normal file
14
app/entities/post_request.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Adamantium
|
||||
module Entities
|
||||
class PostRequest < Dry::Struct
|
||||
attribute :h, Types::Coercible::String
|
||||
attribute :action, Types::Coercible::String.optional
|
||||
attribute :name, Types::Coercible::String.optional
|
||||
attribute :content, Types::Coercible::String
|
||||
attribute :slug, Types::Coercible::String
|
||||
attribute :category, Types::Array.of(Types::Coercible::String)
|
||||
attribute :published_at, Types::Nominal::DateTime.optional
|
||||
attribute :post_type, Types::Coercible::String
|
||||
end
|
||||
end
|
||||
end
|
84
app/repos/post_repo.rb
Normal file
84
app/repos/post_repo.rb
Normal file
@@ -0,0 +1,84 @@
|
||||
module Adamantium
|
||||
module Repos
|
||||
class PostRepo < Adamantium::Repo[:posts]
|
||||
commands :update
|
||||
|
||||
def create(post_attrs)
|
||||
posts.transaction do
|
||||
new_post = posts.changeset(:create, post_attrs).commit
|
||||
|
||||
post_attrs[:category].each do |tag_name|
|
||||
next if tag_name == ""
|
||||
|
||||
tag = posts.tags.where(label: tag_name).one ||
|
||||
posts
|
||||
.tags
|
||||
.changeset(:create, {label: tag_name, slug: tag_name.downcase.strip.tr(" ", "-").gsub(/[^\w-]/, "")})
|
||||
.commit
|
||||
|
||||
posts.post_tags.changeset(:create, {
|
||||
post_id: new_post.id,
|
||||
tag_id: tag[:id]
|
||||
})
|
||||
.commit
|
||||
end
|
||||
|
||||
new_post
|
||||
end
|
||||
end
|
||||
|
||||
def post_listing(limit: nil)
|
||||
posts
|
||||
.where(post_type: "post")
|
||||
.published
|
||||
.combine(:tags)
|
||||
.order(Sequel.desc(:published_at))
|
||||
.limit(limit)
|
||||
.to_a
|
||||
end
|
||||
|
||||
def bookmark_listing(query: nil)
|
||||
base = posts
|
||||
.where(post_type: "bookmark")
|
||||
.published
|
||||
.combine(:tags)
|
||||
.order(Sequel.desc(:published_at))
|
||||
|
||||
query ? base.where(Sequel.ilike(:name, "%#{query}%")).to_a : base.to_a
|
||||
end
|
||||
|
||||
def for_rss
|
||||
posts
|
||||
.where(post_type: "post")
|
||||
.published
|
||||
.combine(:tags)
|
||||
.order(Sequel.desc(:published_at))
|
||||
.to_a
|
||||
end
|
||||
|
||||
def fetch!(slug)
|
||||
posts
|
||||
.published
|
||||
.combine(:tags)
|
||||
.where(slug: slug)
|
||||
.one!
|
||||
end
|
||||
|
||||
def slug_exists?(slug)
|
||||
!!posts
|
||||
.where(slug: slug)
|
||||
.one
|
||||
end
|
||||
|
||||
def delete!(slug)
|
||||
delete_post = posts.where(slug: slug).command(:update)
|
||||
delete_post.call(published_at: nil)
|
||||
end
|
||||
|
||||
def restore!(slug)
|
||||
delete_post = posts.where(slug: slug).command(:update)
|
||||
delete_post.call(published_at: Time.now)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
26
app/repos/post_tag_repo.rb
Normal file
26
app/repos/post_tag_repo.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
module Adamantium
|
||||
module Repos
|
||||
class PostTagRepo < Adamantium::Repo[:post_tags]
|
||||
def posts_tagged(tag:)
|
||||
tag_id = post_tags
|
||||
.tags
|
||||
.where(slug: tag)
|
||||
.one!
|
||||
.id
|
||||
|
||||
post_ids = post_tags
|
||||
.where(tag_id: tag_id)
|
||||
.to_a
|
||||
.map(&:post_id)
|
||||
|
||||
post_tags
|
||||
.posts
|
||||
.where(id: post_ids)
|
||||
.published
|
||||
.combine(:tags)
|
||||
.order(Sequel.desc(:published_at))
|
||||
.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
app/repos/tag_repo.rb
Normal file
9
app/repos/tag_repo.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
module Adamantium
|
||||
module Repos
|
||||
class TagRepo < Adamantium::Repo[:tags]
|
||||
def fetch!(slug)
|
||||
tags.where(slug: slug).one!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/templates/bookmarks/index.html.slim
Normal file
13
app/templates/bookmarks/index.html.slim
Normal file
@@ -0,0 +1,13 @@
|
||||
div class="flex justify-between mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
div class="basis-1/5"
|
||||
h1 Bookmarks
|
||||
|
||||
form class="basis-2/5" method="GET" action="/bookmarks"
|
||||
input class="w-48 border-blue-400 border-2 rounded mr-2 px-2" id="seach" type="text" name="q" value=q
|
||||
button class="w-16 border-blue-400 border-2 rounded bg-blue-400 hover:bg-blue-800 hover:border-blue-800 hover:text-blue-100 px-1 text-gray-200" type="submit" Search
|
||||
|
||||
div class="mb-12 max-w-prose mx-auto"
|
||||
- bookmarks.each do |bookmark|
|
||||
== render :bookmark, bookmark: bookmark
|
||||
|
||||
div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
17
app/templates/bookmarks/show.html.slim
Normal file
17
app/templates/bookmarks/show.html.slim
Normal file
@@ -0,0 +1,17 @@
|
||||
div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
h1 = bookmark.name
|
||||
|
||||
div class="mb-12 prose max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
a class="text-blue-600 no-underline hover:underline" href=bookmark.url
|
||||
p class="text-xl"
|
||||
= bookmark.url
|
||||
|
||||
== bookmark.content
|
||||
|
||||
div class="mb-8 max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
||||
|
||||
div class="mb-2 max-w-prose mx-auto text-gray-800 dark:text-gray-200 flex"
|
||||
= "Published #{bookmark.display_published_at}"
|
||||
span class="text-right flex-1"
|
||||
== render :tags, tags: bookmark.tags
|
||||
|
20
app/templates/feeds/rss.xml.builder
Normal file
20
app/templates/feeds/rss.xml.builder
Normal file
@@ -0,0 +1,20 @@
|
||||
xml.instruct!(:xml, version: "2.0", encoding: "utf-8")
|
||||
|
||||
xml.channel do |channel|
|
||||
channel.title "Daniel Nitsikopoulos"
|
||||
channel.description "The RSS feed for https://dnitza.com"
|
||||
channel.lastBuildDate Time.now.rfc2822
|
||||
channel.pubDate Time.now.rfc2822
|
||||
channel.ttl 1800
|
||||
|
||||
posts.each do |post|
|
||||
channel.item do |item|
|
||||
item.title post.display_title
|
||||
item.description do |desc|
|
||||
desc.cdata! post.content
|
||||
end
|
||||
item.guid(post.slug, isPermaLink: true)
|
||||
item.pubDate post.machine_published_at
|
||||
end
|
||||
end
|
||||
end
|
46
app/templates/layouts/app.html.slim
Normal file
46
app/templates/layouts/app.html.slim
Normal file
@@ -0,0 +1,46 @@
|
||||
html
|
||||
head
|
||||
meta charest="utf-8"
|
||||
|
||||
meta name="viewport" content="width=device-width, initial-scale=1.0"
|
||||
|
||||
title Daniel Nitsikopoulos
|
||||
|
||||
link rel="authorization_endpoint" href=Hanami.app.settings.micropub_authorization_endpoint
|
||||
link rel="token_endpoint" href=Hanami.app.settings.micropub_token_endpoint
|
||||
link rel="micropub" href="#{URI.join(Hanami.app.settings.micropub_site_url, "micropub")}"
|
||||
|
||||
link rel="stylesheet" href="/assets/index.css"
|
||||
|
||||
script data-domain="dnitza.com" src="https://stats.dnitza.com/js/script.js" defer="true"
|
||||
|
||||
/ script src="https://unpkg.com/htmx.org@1.8.4" integrity="sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" crossorigin="anonymous"
|
||||
|
||||
- if Hanami.app.settings.micropub_pub_key
|
||||
link rel="pgpkey" href="/key"
|
||||
body class="bg-white dark:bg-black selection:bg-blue-100 selection:text-blue-900 dark:selection:bg-blue-600 dark:selection:text-blue-100"
|
||||
main class="pb-8 px-4 pt-4 md:pt-8"
|
||||
header class="mb-12 max-w-screen-md mx-auto"
|
||||
div class="flex items-center mb-8 md:mb-12 text-lg md:text-xl text-gray-400 dark:text-gray-600"
|
||||
div class="flex-none mx-auto md:flex-auto md:mx-0"
|
||||
div class="h-card flex items-center"
|
||||
img class="u-photo w-6 h6 md:w-10 md:h-10 rounded-full mr-1.5" src="/assets/memoji.png"
|
||||
a href="/"
|
||||
h1 class="p-bane uppercase text-sm md:text-sm text-gray-400 dark:text-gray-400" = Hanami.app.settings.site_name
|
||||
nav class="space-x-1 text-sm md:text-sm uppercase md:block"
|
||||
a class="p-1 rounded text-gray-400 hover:bg-red-100 hover:text-red-400 dark:hover:bg-red-200 #{link_active?('about') ? 'text-red-600 dark:text-red-400' : ''}" href="/about" About
|
||||
span class="text-gray-400 dark:text-gray-600"
|
||||
= "/"
|
||||
a class="p-1 rounded text-gray-400 hover:bg-blue-100 hover:text-blue-400 dark:hover:bg-blue-200" href="/posts" Writing
|
||||
span class="text-gray-400 dark:text-gray-600"
|
||||
= "/"
|
||||
a class="p-1 rounded text-gray-400 hover:bg-yellow-100 hover:text-yellow-600 dark:hover:bg-yellow-200 #{link_active?('bookmarks') ? 'text-yellow-600 dark:text-yellow-600' : ''}" href="/bookmarks" Bookmarks
|
||||
span class="text-gray-400 dark:text-gray-600"
|
||||
= "/"
|
||||
a class="p-1 rounded text-gray-400 hover:bg-orange-100 hover:text-orange-400 dark:hover:bg-orange-200" href="#{Hanami.app.settings.micropub_site_url}/feeds/rss" RSS
|
||||
// span class="text-gray-400 dark:text-gray-600"
|
||||
= "/"
|
||||
// a class="text-green-600 hover:text-gray-800 dark:hover:text-gray-200" href="/art-things" Art things
|
||||
== yield
|
||||
div class="px-4 max-w-screen-md mx-auto pb-10"
|
||||
p class="text-gray-200 dark:text-gray-600" © 2023 Daniel Nitsikopoulos. All rights reserved.
|
0
app/templates/layouts/app.xml.builder
Normal file
0
app/templates/layouts/app.xml.builder
Normal file
3
app/templates/not_found.html.slim
Normal file
3
app/templates/not_found.html.slim
Normal file
@@ -0,0 +1,3 @@
|
||||
div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
h1 Not Found!
|
||||
|
4
app/templates/pages/show.html.slim
Normal file
4
app/templates/pages/show.html.slim
Normal file
@@ -0,0 +1,4 @@
|
||||
article class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200 prose-em:font-bold prose-em:not-italic prose-em:bg-blue-600 prose-em:px-1 prose-a:text-blue-600 prose-a:p-0.5 prose-a:rounded-sm prose-a:no-underline hover:prose-a:underline prose-em:text-blue-100"
|
||||
== page_content
|
||||
|
||||
div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
8
app/templates/posts/index.html.slim
Normal file
8
app/templates/posts/index.html.slim
Normal file
@@ -0,0 +1,8 @@
|
||||
div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
h1 Writing
|
||||
|
||||
div class="mb-12 max-w-prose mx-auto"
|
||||
- posts.each do |post|
|
||||
== render :post, post: post
|
||||
|
||||
div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
12
app/templates/posts/show.html.slim
Normal file
12
app/templates/posts/show.html.slim
Normal file
@@ -0,0 +1,12 @@
|
||||
div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
h1 = post.display_title
|
||||
|
||||
article class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200 prose-a:text-blue-600 prose-a:no-underline hover:prose-a:underline"
|
||||
== post.content
|
||||
|
||||
div class="mb-4 max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
||||
|
||||
div class="mb-2 max-w-prose mx-auto text-gray-600 dark:text-gray-200 flex"
|
||||
= "Published #{post.display_published_at}"
|
||||
span class="text-right flex-1"
|
||||
== render :tags, tags: post.tags
|
12
app/templates/shared/_bookmark.html.slim
Normal file
12
app/templates/shared/_bookmark.html.slim
Normal file
@@ -0,0 +1,12 @@
|
||||
div class="mb-8"
|
||||
h3 class="text-xl font-bold text-blue-600 hover:underline"
|
||||
a href="#{bookmark.url}"
|
||||
= "🔖 #{bookmark.name} "
|
||||
== render("link_arrow")
|
||||
p class="e-content leading-relaxed md:text-lg text-gray-800 dark:text-gray-200"
|
||||
= bookmark.content
|
||||
== render :tags, tags: bookmark.tags
|
||||
|
||||
p class="text-sm u-url text-blue-400 hover:text-blue-600 dark:hover:text-blue-200"
|
||||
a href="/bookmark/#{bookmark.slug}"
|
||||
= bookmark.display_published_at
|
4
app/templates/shared/_link_arrow.html.slim
Normal file
4
app/templates/shared/_link_arrow.html.slim
Normal file
@@ -0,0 +1,4 @@
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="inline w-2 h2 md:w-4 md:h-4 mt-0.5"
|
||||
g
|
||||
rect height="14.4434" opacity="0" width="14.4238" x="0" y="0"
|
||||
path d="M14.4238 10.8008L14.4141 0.976562C14.4141 0.419922 14.0527 0.0292969 13.4668 0.0292969L3.64258 0.0292969C3.0957 0.0292969 2.72461 0.449219 2.72461 0.917969C2.72461 1.38672 3.14453 1.78711 3.60352 1.78711L7.00195 1.78711L11.7676 1.63086L9.95117 3.22266L0.273438 12.9199C0.0976562 13.0957 0 13.3203 0 13.5352C0 14.0039 0.419922 14.4434 0.908203 14.4434C1.13281 14.4434 1.34766 14.3652 1.52344 14.1797L11.2207 4.49219L12.832 2.66602L12.6562 7.22656L12.6562 10.8398C12.6562 11.2988 13.0566 11.7285 13.5352 11.7285C14.0039 11.7285 14.4238 11.3281 14.4238 10.8008Z"
|
10
app/templates/shared/_post.html.slim
Normal file
10
app/templates/shared/_post.html.slim
Normal file
@@ -0,0 +1,10 @@
|
||||
div class="mb-8"
|
||||
h3 class="text-xl font-bold text-blue-600 hover:underline"
|
||||
a href="/post/#{post.slug}"
|
||||
= post.display_title
|
||||
div class="text-base text-gray-800 dark:text-gray-200"
|
||||
== post.excerpt
|
||||
== render :tags, tags: post.tags
|
||||
|
||||
p class="text-sm text-blue-400"
|
||||
= post.display_published_at
|
8
app/templates/shared/_tags.html.slim
Normal file
8
app/templates/shared/_tags.html.slim
Normal file
@@ -0,0 +1,8 @@
|
||||
- if tags.count > 0
|
||||
div class="mb-2"
|
||||
span class="text-sm text-gray-600 dark:text-gray-200"
|
||||
= "Tagged: "
|
||||
- tags.each do |tag|
|
||||
span
|
||||
a class="rounded p-1 mr-1 text-xsm u-url bg-yellow-200/60 hover:bg-yellow-200 dark:bg-yellow-400 dark:hover:bg-yellow-400/80 dark:text-yellow-800 dark:hover:text-yellow-100 text-gray-600" href="/tagged/#{tag.slug}"
|
||||
= tag.label
|
14
app/templates/site/home.html.slim
Normal file
14
app/templates/site/home.html.slim
Normal file
@@ -0,0 +1,14 @@
|
||||
div class="mb-12 max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
== home_content
|
||||
|
||||
div class="mb-8 max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
||||
|
||||
div class="mb-4 flex max-w-prose mx-auto"
|
||||
h2 class="text-xl text-gray-600 dark:text-gray-200" Recent
|
||||
a class="text-right flex-1 text-blue-400" href="/posts" See all →
|
||||
|
||||
div class="mb-12 max-w-prose mx-auto"
|
||||
- posts.each do |post|
|
||||
== render :post, post: post
|
||||
|
||||
div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
8
app/templates/tags/show.html.slim
Normal file
8
app/templates/tags/show.html.slim
Normal file
@@ -0,0 +1,8 @@
|
||||
div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200"
|
||||
h1 = "Tagged: \"#{tag.label}\""
|
||||
|
||||
div class="mb-12 max-w-prose mx-auto"
|
||||
- posts.each do |post|
|
||||
== render post.post_type.to_sym, post.post_type.to_sym => post
|
||||
|
||||
div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600"
|
17
app/validation/posts/bookmark_contract.rb
Normal file
17
app/validation/posts/bookmark_contract.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module Adamantium
|
||||
module Validation
|
||||
module Posts
|
||||
class BookmarkContract < Dry::Validation::Contract
|
||||
params do
|
||||
required(:name).filled(:string)
|
||||
required(:content).maybe(:string)
|
||||
required(:category).array(:string)
|
||||
required(:published_at).maybe(:time)
|
||||
required(:url).filled(:string)
|
||||
required(:slug).filled(:string)
|
||||
required(:post_type).value(included_in?: %w[bookmark])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
app/validation/posts/post_contract.rb
Normal file
16
app/validation/posts/post_contract.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module Adamantium
|
||||
module Validation
|
||||
module Posts
|
||||
class PostContract < Dry::Validation::Contract
|
||||
params do
|
||||
required(:name).maybe(:string)
|
||||
required(:content).filled(:string)
|
||||
required(:category).array(:string)
|
||||
required(:published_at).maybe(:time)
|
||||
required(:slug).filled(:string)
|
||||
required(:post_type).value(included_in?: %w[post])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
app/views/bookmarks/index.rb
Normal file
19
app/views/bookmarks/index.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Bookmarks
|
||||
class Index < Adamantium::View
|
||||
include Deps["repos.post_repo"]
|
||||
|
||||
expose :bookmarks do |query:|
|
||||
post_repo.bookmark_listing(query: query).map do |bookmark|
|
||||
Decorators::Bookmarks::Decorator.new bookmark
|
||||
end
|
||||
end
|
||||
|
||||
expose :q do |query:|
|
||||
query
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/views/bookmarks/show.rb
Normal file
13
app/views/bookmarks/show.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Bookmarks
|
||||
class Show < Adamantium::View
|
||||
include Deps["repos.post_repo"]
|
||||
|
||||
expose :bookmark do |slug:|
|
||||
Decorators::Bookmarks::Decorator.new(post_repo.fetch!(slug))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
app/views/feeds/rss.rb
Normal file
23
app/views/feeds/rss.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require "builder"
|
||||
|
||||
module Adamantium
|
||||
module Views
|
||||
module Feeds
|
||||
class Rss < Adamantium::View
|
||||
include Deps["repos.post_repo"]
|
||||
|
||||
expose :posts do
|
||||
post_repo.for_rss.map do |post|
|
||||
Decorators::Posts::Decorator.new post
|
||||
end
|
||||
end
|
||||
|
||||
expose :xml, decorate: false, layout: true
|
||||
|
||||
def xml
|
||||
Builder::XmlMarkup.new(indent: 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
6
app/views/not_found.rb
Normal file
6
app/views/not_found.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
class NotFound < View
|
||||
end
|
||||
end
|
||||
end
|
17
app/views/pages/show.rb
Normal file
17
app/views/pages/show.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Pages
|
||||
class Show < Adamantium::View
|
||||
include Deps[renderer: "renderers.markdown"]
|
||||
|
||||
expose :page_content do |slug:|
|
||||
markdown_content = File.read("app/content/pages/#{slug}.md")
|
||||
|
||||
renderer.call(content: markdown_content)
|
||||
rescue Errno::ENOENT
|
||||
renderer.call(content: "## Page not found")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
15
app/views/posts/index.rb
Normal file
15
app/views/posts/index.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Posts
|
||||
class Index < Adamantium::View
|
||||
include Deps["repos.post_repo"]
|
||||
|
||||
expose :posts do
|
||||
post_repo.post_listing.map do |post|
|
||||
Decorators::Posts::Decorator.new(post)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
13
app/views/posts/show.rb
Normal file
13
app/views/posts/show.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Posts
|
||||
class Show < Adamantium::View
|
||||
include Deps["repos.post_repo"]
|
||||
|
||||
expose :post do |slug:|
|
||||
Decorators::Posts::Decorator.new(post_repo.fetch!(slug))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
app/views/site/home.rb
Normal file
21
app/views/site/home.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Site
|
||||
class Home < Adamantium::View
|
||||
include Deps["repos.post_repo", renderer: "renderers.markdown"]
|
||||
|
||||
expose :home_content do
|
||||
markdown_content = File.read("app/content/home.md")
|
||||
|
||||
renderer.call(content: markdown_content)
|
||||
end
|
||||
|
||||
expose :posts do
|
||||
post_repo.post_listing(limit: 10).map do |post|
|
||||
Decorators::Posts::Decorator.new(post)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
22
app/views/tags/show.rb
Normal file
22
app/views/tags/show.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module Adamantium
|
||||
module Views
|
||||
module Tags
|
||||
class Show < Adamantium::View
|
||||
include Deps[
|
||||
"repos.post_tag_repo",
|
||||
"repos.tag_repo"
|
||||
]
|
||||
|
||||
expose :posts do |slug:|
|
||||
post_tag_repo.posts_tagged(tag: slug).map do |post|
|
||||
Decorators::Posts::Decorator.new(post)
|
||||
end
|
||||
end
|
||||
|
||||
expose :tag do |slug:|
|
||||
tag_repo.fetch!(slug)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
29
bin/hanami
Executable file
29
bin/hanami
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/setup"
|
||||
require "hanami/cli"
|
||||
|
||||
# Hanami 2.0 does does not officially include database integration. However, much of the required
|
||||
# work is already done and included in the gem.
|
||||
#
|
||||
# This CLI shim activates the database commands so we can manage our app database.
|
||||
|
||||
Hanami::CLI.tap do |cli|
|
||||
cli.register "db create", Hanami::CLI::Commands::App::DB::Create
|
||||
cli.register "db create_migration", Hanami::CLI::Commands::App::DB::CreateMigration
|
||||
cli.register "db drop", Hanami::CLI::Commands::App::DB::Drop
|
||||
cli.register "db migrate", Hanami::CLI::Commands::App::DB::Migrate
|
||||
cli.register "db setup", Hanami::CLI::Commands::App::DB::Setup
|
||||
cli.register "db reset", Hanami::CLI::Commands::App::DB::Reset
|
||||
cli.register "db rollback", Hanami::CLI::Commands::App::DB::Rollback
|
||||
cli.register "db sample_data", Hanami::CLI::Commands::App::DB::SampleData
|
||||
cli.register "db seed", Hanami::CLI::Commands::App::DB::Seed
|
||||
cli.register "db structure dump", Hanami::CLI::Commands::App::DB::Structure::Dump
|
||||
cli.register "db version", Hanami::CLI::Commands::App::DB::Version
|
||||
end
|
||||
|
||||
Hanami::CLI::Bundler.require(:cli)
|
||||
|
||||
cli = Dry::CLI.new(Hanami::CLI)
|
||||
cli.call
|
7
config.ru
Normal file
7
config.ru
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "hanami/boot"
|
||||
|
||||
use Rack::Static, urls: ["/assets", "/media"], root: "public"
|
||||
|
||||
run Hanami.app
|
11
config/app.rb
Normal file
11
config/app.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "hanami"
|
||||
|
||||
module Adamantium
|
||||
class App < Hanami::App
|
||||
config.actions.content_security_policy[:script_src] += " https://gist.github.com"
|
||||
config.actions.content_security_policy[:script_src] += " *.dnitza.com"
|
||||
config.actions.content_security_policy[:connect_src] += " https://stats.dnitza.com/api/event"
|
||||
end
|
||||
end
|
0
config/nginx.conf
Normal file
0
config/nginx.conf
Normal file
7
config/providers/param_parser.rb
Normal file
7
config/providers/param_parser.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Hanami.app.register_provider :param_parser, namespace: true do
|
||||
start do
|
||||
register "micropub_post", Adamantium::MicropubRequestParser.new
|
||||
end
|
||||
end
|
41
config/providers/persistence.rb
Normal file
41
config/providers/persistence.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Hanami.app.register_provider :persistence, namespace: true do
|
||||
prepare do
|
||||
require "rom-changeset"
|
||||
require "rom/core"
|
||||
require "rom/sql"
|
||||
|
||||
# TODO(Hanami): As part of built-in rom setup, configure ROM with app inflector
|
||||
silence_warnings { ROM::Inflector = Hanami.app["inflector"] }
|
||||
|
||||
rom_config = ROM::Configuration.new(:sql, target["settings"].database_url)
|
||||
|
||||
rom_config.plugin(:sql, relations: :instrumentation) do |plugin_config|
|
||||
plugin_config.notifications = target["notifications"]
|
||||
end
|
||||
|
||||
rom_config.plugin(:sql, relations: :auto_restrictions)
|
||||
|
||||
register "config", rom_config
|
||||
register "db", rom_config.gateways[:default].connection
|
||||
end
|
||||
|
||||
start do
|
||||
rom_config = target["persistence.config"]
|
||||
rom_config.auto_registration(
|
||||
target.root.join("lib/adamantium/persistence"),
|
||||
namespace: "Adamantium::Persistence"
|
||||
)
|
||||
|
||||
register "rom", ROM.container(rom_config)
|
||||
end
|
||||
|
||||
define_method(:silence_warnings) do |&block|
|
||||
orig_verbose = $VERBOSE
|
||||
$VERBOSE = nil
|
||||
result = block.call
|
||||
$VERBOSE = orig_verbose
|
||||
result
|
||||
end
|
||||
end
|
7
config/providers/post_utilities.rb
Normal file
7
config/providers/post_utilities.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Hanami.app.register_provider :post_utilities, namespace: true do
|
||||
start do
|
||||
register "slugify", Adamantium::SlugCreator.new
|
||||
end
|
||||
end
|
7
config/providers/renderers.rb
Normal file
7
config/providers/renderers.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Hanami.app.register_provider :renderers, namespace: true do
|
||||
start do
|
||||
register "markdown", Adamantium::Renderer::Markdown.new
|
||||
end
|
||||
end
|
15
config/puma.rb
Normal file
15
config/puma.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
max_threads_count = ENV.fetch("HANAMI_MAX_THREADS", 5)
|
||||
min_threads_count = ENV.fetch("HANAMI_MIN_THREADS") { max_threads_count }
|
||||
threads min_threads_count, max_threads_count
|
||||
|
||||
port ENV.fetch("HANAMI_PORT", 2300)
|
||||
environment ENV.fetch("HANAMI_ENV", "development")
|
||||
workers ENV.fetch("HANAMI_WEB_CONCURRENCY", 2)
|
||||
|
||||
on_worker_boot do
|
||||
Hanami.shutdown
|
||||
end
|
||||
|
||||
preload_app!
|
35
config/routes.rb
Normal file
35
config/routes.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "hanami/middleware/body_parser"
|
||||
require "adamantium/middleware/process_params"
|
||||
|
||||
module Adamantium
|
||||
class Routes < Hanami::Routes
|
||||
use Hanami::Middleware::BodyParser, [:form, :json]
|
||||
use Adamantium::Middleware::ProcessParams
|
||||
|
||||
scope "micropub" do
|
||||
get "/", to: "site.config"
|
||||
post "/", to: "posts.handle"
|
||||
post "/media", to: "media.create"
|
||||
end
|
||||
|
||||
get "/", to: "site.home"
|
||||
get "/post/:slug", to: "posts.show"
|
||||
get "/posts", to: "posts.index"
|
||||
|
||||
get "/bookmarks", to: "bookmarks.index"
|
||||
get "/bookmark/:slug", to: "bookmarks.show"
|
||||
|
||||
get "/tagged/:slug", to: "tags.show"
|
||||
|
||||
get "/key", to: "key.show" if Hanami.app.settings.micropub_pub_key
|
||||
|
||||
get "/feeds/rss", to: "feeds.rss"
|
||||
|
||||
get "/:slug", to: "pages.show"
|
||||
|
||||
redirect "deploying-a-hanami-app-to-fly-io", to: "/post/deploying-a-hanami-20-app-to-flyio"
|
||||
redirect "deploying-a-hanami-app-to-fly-io/", to: "/post/deploying-a-hanami-20-app-to-flyio"
|
||||
end
|
||||
end
|
29
config/settings.rb
Normal file
29
config/settings.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "adamantium/types"
|
||||
|
||||
module Adamantium
|
||||
class Settings < Hanami::Settings
|
||||
# Infrastructure
|
||||
setting :database_url
|
||||
|
||||
# Site details
|
||||
setting :site_name
|
||||
|
||||
## ---- Micropub ----
|
||||
|
||||
# Site details
|
||||
setting :micropub_site_id
|
||||
setting :micropub_site_name
|
||||
setting :micropub_site_url
|
||||
# Auth
|
||||
setting :micropub_pub_key, default: nil
|
||||
# TODO: add other auth methods here
|
||||
|
||||
# Micropub endpoints
|
||||
setting :micropub_media_endpoint, default: "", constructor: Types::Params::String
|
||||
|
||||
setting :micropub_authorization_endpoint
|
||||
setting :micropub_token_endpoint
|
||||
end
|
||||
end
|
12
db/migrate/20230101035642_create_posts.rb
Normal file
12
db/migrate/20230101035642_create_posts.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
create_table :posts do
|
||||
primary_key :id
|
||||
column :name, :text, null: false, default: ""
|
||||
column :content, :text, null: false
|
||||
column :published_at, :timestamp
|
||||
end
|
||||
end
|
||||
end
|
9
db/migrate/20230101113513_add_slug_to_posts.rb
Normal file
9
db/migrate/20230101113513_add_slug_to_posts.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
alter_table :posts do
|
||||
add_column :slug, :text, null: false
|
||||
end
|
||||
end
|
||||
end
|
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
alter_table :posts do
|
||||
add_unique_constraint :slug
|
||||
end
|
||||
end
|
||||
end
|
13
db/migrate/20230102075558_create_bookmarks.rb
Normal file
13
db/migrate/20230102075558_create_bookmarks.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
create_table :bookmarks do
|
||||
primary_key :id
|
||||
column :url, :text, null: false
|
||||
column :name, :text, null: false
|
||||
column :content, :text, null: false, default: ""
|
||||
column :published_at, :timestamp
|
||||
end
|
||||
end
|
||||
end
|
9
db/migrate/20230103085913_add_slug_to_bookmarks.rb
Normal file
9
db/migrate/20230103085913_add_slug_to_bookmarks.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
alter_table :bookmarks do
|
||||
add_column :slug, :text, null: false, unique: true
|
||||
end
|
||||
end
|
||||
end
|
16
db/migrate/20230103215123_create_tags.rb
Normal file
16
db/migrate/20230103215123_create_tags.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
create_table :tags do
|
||||
primary_key :id
|
||||
column :label, :text, null: false, unique: true
|
||||
column :slug, :text, null: false, unique: true
|
||||
end
|
||||
|
||||
create_table :post_tags do
|
||||
foreign_key :post_id, :posts, null: false
|
||||
foreign_key :tag_id, :tags, null: false
|
||||
end
|
||||
end
|
||||
end
|
11
db/migrate/20230103215311_combine_posts_and_bookmarks.rb
Normal file
11
db/migrate/20230103215311_combine_posts_and_bookmarks.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
drop_table :bookmarks
|
||||
alter_table :posts do
|
||||
add_column :url, :text
|
||||
add_column :post_type, :text
|
||||
end
|
||||
end
|
||||
end
|
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
alter_table :posts do
|
||||
set_column_allow_null :content
|
||||
end
|
||||
end
|
||||
end
|
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
ROM::SQL.migration do
|
||||
change do
|
||||
alter_table :posts do
|
||||
set_column_allow_null :name
|
||||
end
|
||||
end
|
||||
end
|
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- .:/app
|
||||
- bundle_path:/usr/local/bundle
|
||||
command: script/puma
|
||||
env_file: ".env"
|
||||
ports:
|
||||
- 2300:2300
|
||||
volumes:
|
||||
bundle_path:
|
||||
node_modules:
|
13
lib/adamantium/command.rb
Normal file
13
lib/adamantium/command.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# auto_register: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "dry-matcher"
|
||||
require "dry/matcher/result_matcher"
|
||||
require "dry-monads"
|
||||
|
||||
module Adamantium
|
||||
class Command
|
||||
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
||||
include Dry::Monads[:result]
|
||||
end
|
||||
end
|
14
lib/adamantium/context.rb
Normal file
14
lib/adamantium/context.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Adamantium
|
||||
class Context < Hanami::View::Context
|
||||
def initialize(**options)
|
||||
@options = options
|
||||
super(**options)
|
||||
end
|
||||
|
||||
def link_active? path
|
||||
# TODO: waiting for Hanami View to be released
|
||||
# to access current_path
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
85
lib/adamantium/micropub_request_parser.rb
Normal file
85
lib/adamantium/micropub_request_parser.rb
Normal file
@@ -0,0 +1,85 @@
|
||||
require "securerandom"
|
||||
|
||||
module Adamantium
|
||||
class MicropubRequestParser
|
||||
include Deps["post_utilities.slugify", "repos.post_repo"]
|
||||
|
||||
def call(params:)
|
||||
return nil if params.key?(:action)
|
||||
|
||||
cont_type = content_type(params)
|
||||
req_type = request_type(params)
|
||||
req_params = parse_params(req_type, cont_type, params)
|
||||
|
||||
if cont_type == :bookmark
|
||||
return Entities::BookmarkRequest.new(req_params)
|
||||
end
|
||||
|
||||
Entities::PostRequest.new(req_params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def slug(name:, default_slug:)
|
||||
return default_slug if default_slug
|
||||
|
||||
slugify.call(
|
||||
text: name,
|
||||
checker: post_repo.method(:slug_exists?)
|
||||
)
|
||||
end
|
||||
|
||||
def content_type(params)
|
||||
return :bookmark if params[:"bookmark-of"]
|
||||
:post
|
||||
end
|
||||
|
||||
def request_type(params)
|
||||
if params[:h] == "entry"
|
||||
return :form
|
||||
end
|
||||
|
||||
if params[:type]&.include?("h-entry")
|
||||
return :json
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def parse_params(req_type, post_type, params)
|
||||
new_params = {}
|
||||
new_params[:h] = "entry"
|
||||
new_params[:post_type] = post_type
|
||||
new_params[:action] = params[:action]
|
||||
|
||||
publish_time = params[:published_at] || Time.now
|
||||
|
||||
if req_type == :json
|
||||
new_params[:published_at] = (params[:"post-status"] == "draft") ? nil : publish_time
|
||||
new_params[:category] = params[:properties][:category] || []
|
||||
new_params[:name] = params[:properties][:name] && params[:properties][:name].first
|
||||
new_params[:content] = params[:properties][:content]&.first&.tr("\n", " ")
|
||||
new_params[:slug] = params[:slug]
|
||||
|
||||
else
|
||||
new_params[:name] = params[:name]
|
||||
new_params[:published_at] = (params[:"post-status"] == "draft") ? nil : publish_time
|
||||
new_params[:category] = params[:category] || []
|
||||
|
||||
content = if params[:content]
|
||||
if params[:content].is_a?(Hash) && params[:content][:html]
|
||||
params[:content][:html]
|
||||
else
|
||||
params[:content]
|
||||
end
|
||||
end
|
||||
|
||||
new_params[:content] = content
|
||||
end
|
||||
new_params[:url] = params[:"bookmark-of"]
|
||||
new_params[:slug] = slug(name: new_params[:name], default_slug: params[:slug])
|
||||
|
||||
new_params
|
||||
end
|
||||
end
|
||||
end
|
14
lib/adamantium/middleware/process_params.rb
Normal file
14
lib/adamantium/middleware/process_params.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Adamantium
|
||||
module Middleware
|
||||
class ProcessParams
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
# NOOP for now.
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
18
lib/adamantium/persistence/relations/post_tags.rb
Normal file
18
lib/adamantium/persistence/relations/post_tags.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Adamantium
|
||||
module Persistence
|
||||
module Relations
|
||||
class PostTags < ROM::Relation[:sql]
|
||||
schema :post_tags, infer: true do
|
||||
associations do
|
||||
belongs_to :post
|
||||
belongs_to :tag
|
||||
end
|
||||
end
|
||||
|
||||
auto_struct(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
22
lib/adamantium/persistence/relations/posts.rb
Normal file
22
lib/adamantium/persistence/relations/posts.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Adamantium
|
||||
module Persistence
|
||||
module Relations
|
||||
class Posts < ROM::Relation[:sql]
|
||||
schema :posts, infer: true do
|
||||
associations do
|
||||
has_many :post_tags
|
||||
has_many :tags, through: :post_tags
|
||||
end
|
||||
end
|
||||
|
||||
auto_struct(true)
|
||||
|
||||
def published
|
||||
where(self[:published_at] <= Time.now)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
18
lib/adamantium/persistence/relations/tags.rb
Normal file
18
lib/adamantium/persistence/relations/tags.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Adamantium
|
||||
module Persistence
|
||||
module Relations
|
||||
class Tags < ROM::Relation[:sql]
|
||||
schema :tags, infer: true do
|
||||
associations do
|
||||
belongs_to :post_tag
|
||||
belongs_to :post, through: :post_tag
|
||||
end
|
||||
end
|
||||
|
||||
auto_struct(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
lib/adamantium/renderer/markdown.rb
Normal file
21
lib/adamantium/renderer/markdown.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
require "redcarpet"
|
||||
|
||||
module Adamantium
|
||||
module Renderer
|
||||
class Markdown
|
||||
attr_accessor :markdown
|
||||
|
||||
def initialize
|
||||
renderer = Redcarpet::Render::HTML.new({})
|
||||
extensions = {
|
||||
fenced_code_blocks: true
|
||||
}
|
||||
@markdown = Redcarpet::Markdown.new(renderer, extensions)
|
||||
end
|
||||
|
||||
def call(content:)
|
||||
markdown.render(content)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
10
lib/adamantium/repo.rb
Normal file
10
lib/adamantium/repo.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# auto_register: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rom-repository"
|
||||
|
||||
module Adamantium
|
||||
class Repo < ROM::Repository::Root
|
||||
include Deps[container: "persistence.rom"]
|
||||
end
|
||||
end
|
20
lib/adamantium/slug_creator.rb
Normal file
20
lib/adamantium/slug_creator.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require "babosa"
|
||||
require "securerandom"
|
||||
|
||||
module Adamantium
|
||||
class SlugCreator
|
||||
def call(text:, checker:)
|
||||
input_slug = (text != "" && !text.nil?) ? text.to_slug.normalize.to_s : SecureRandom.uuid
|
||||
slug = input_slug
|
||||
|
||||
suffix = 1
|
||||
|
||||
while checker.call(slug)
|
||||
slug = "#{input_slug}-#{suffix}"
|
||||
suffix += 1
|
||||
end
|
||||
|
||||
slug
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user