├── .dockerignore
├── .gitignore
├── .gitmodules
├── .rspec
├── .ruby-version
├── Dockerfile
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Procfile
├── README.md
├── app.json
├── changelog.md
├── config.ru
├── config
└── properties.json
├── docker-compose.yml
├── lib
├── micropublish.rb
└── micropublish
│ ├── auth.rb
│ ├── compare.rb
│ ├── endpoints_finder.rb
│ ├── helpers.rb
│ ├── micropub.rb
│ ├── post.rb
│ ├── request.rb
│ ├── server.rb
│ └── version.rb
├── public
├── css
│ ├── bootstrap-tokenfield.css
│ └── micropublish.css
├── help.md
├── manifest.json
├── micropublish-demo.gif
└── scripts
│ ├── bootstrap-tokenfield.min.js
│ ├── jquery.ns-autogrow.min.js
│ ├── micropublish.js
│ ├── trix.js
│ └── twitter-text.js
├── spec
├── micropublish
│ ├── auth_spec.rb
│ ├── compare_spec.rb
│ ├── endpoints_finder_spec.rb
│ └── server_spec.rb
├── micropublish_spec.rb
└── spec_helper.rb
└── views
├── dashboard.erb
├── delete.erb
├── form.erb
├── layout.erb
├── login.erb
├── preview.erb
├── redirect.erb
├── static.erb
└── undelete.erb
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | Dockerfile
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | .bundle
4 | vendor/bundle
5 | TODO.md
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "vendor/twitter-text"]
2 | path = vendor/twitter-text
3 | url = https://github.com/twitter/twitter-text.git
4 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.3.5
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | # --> Stage 1: Runtime and Build Base Image
4 | ARG RUBY_VERSION=3.3
5 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
6 | WORKDIR /app
7 | SHELL [ "/bin/bash", "-c" ]
8 |
9 | # Install base packages
10 | RUN apt-get update -qq && \
11 | apt-get install --no-install-recommends -y curl libjemalloc2 libvips && \
12 | rm -rf /var/lib/apt/lists /var/cache/apt/archives
13 |
14 | # Set production environment
15 | ENV BUNDLE_DEPLOYMENT="1" \
16 | BUNDLE_PATH="/usr/local/bundle"
17 |
18 | #ENV BUNDLE_WITHOUT="development"
19 |
20 |
21 | # --> Stage 2: Build Environment
22 | FROM base AS build
23 |
24 | # Install packages needed to build gems
25 | RUN apt-get update -qq && \
26 | apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \
27 | rm -rf /var/lib/apt/lists /var/cache/apt/archives
28 |
29 | # Install application gems
30 | COPY Gemfile Gemfile.lock ./
31 | RUN bundle install && \
32 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*# Throw-away build stage to reduce size of final image
33 |
34 | COPY . .
35 |
36 | # --> Stage 3: Actual application deployment
37 | FROM base
38 | COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
39 | COPY --from=build /app /app
40 |
41 | RUN groupadd --system --gid 1000 app && \
42 | useradd app --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
43 | chown -R 1000:1000 .
44 |
45 | USER 1000:1000
46 |
47 | ENV RACK_ENV=production
48 | ENV REDIS_URL=redis://localhost:6379
49 | ENV FORCE_SSL=0
50 |
51 | CMD [ "bundle", "exec", "puma", "-b", "tcp://0.0.0.0:9292" ]
52 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | ruby '~> 3.3.0'
4 |
5 | gem 'sinatra'
6 | gem 'puma'
7 | gem 'rack-contrib'
8 | gem 'rack-ssl'
9 | gem 'link_header'
10 | gem 'httparty'
11 | gem 'nokogiri'
12 | gem 'foreman'
13 | gem 'kramdown'
14 | gem 'redis'
15 | gem 'rackup'
16 |
17 | group :development do
18 | gem 'dotenv'
19 | end
20 |
21 | group :test do
22 | gem 'rack-test'
23 | gem 'rspec'
24 | gem 'webmock'
25 | end
26 |
27 | group :production do
28 | gem 'sentry-raven'
29 | end
30 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.7)
5 | public_suffix (>= 2.0.2, < 7.0)
6 | base64 (0.2.0)
7 | bigdecimal (3.1.8)
8 | connection_pool (2.4.1)
9 | crack (1.0.0)
10 | bigdecimal
11 | rexml
12 | csv (3.3.0)
13 | diff-lcs (1.5.1)
14 | dotenv (3.1.4)
15 | faraday (2.12.0)
16 | faraday-net_http (>= 2.0, < 3.4)
17 | json
18 | logger
19 | faraday-net_http (3.3.0)
20 | net-http
21 | foreman (0.88.1)
22 | hashdiff (1.1.1)
23 | httparty (0.22.0)
24 | csv
25 | mini_mime (>= 1.0.0)
26 | multi_xml (>= 0.5.2)
27 | json (2.8.1)
28 | kramdown (2.4.0)
29 | rexml
30 | link_header (0.0.8)
31 | logger (1.6.1)
32 | mini_mime (1.1.5)
33 | mini_portile2 (2.8.7)
34 | multi_xml (0.7.1)
35 | bigdecimal (~> 3.1)
36 | mustermann (3.0.3)
37 | ruby2_keywords (~> 0.0.1)
38 | net-http (0.5.0)
39 | uri
40 | nio4r (2.7.4)
41 | nokogiri (1.16.7)
42 | mini_portile2 (~> 2.8.2)
43 | racc (~> 1.4)
44 | public_suffix (6.0.1)
45 | puma (6.4.3)
46 | nio4r (~> 2.0)
47 | racc (1.8.1)
48 | rack (3.1.8)
49 | rack-contrib (2.5.0)
50 | rack (< 4)
51 | rack-protection (4.0.0)
52 | base64 (>= 0.1.0)
53 | rack (>= 3.0.0, < 4)
54 | rack-session (2.0.0)
55 | rack (>= 3.0.0)
56 | rack-ssl (1.4.1)
57 | rack
58 | rack-test (2.1.0)
59 | rack (>= 1.3)
60 | rackup (2.2.0)
61 | rack (>= 3)
62 | redis (5.3.0)
63 | redis-client (>= 0.22.0)
64 | redis-client (0.22.2)
65 | connection_pool
66 | rexml (3.3.9)
67 | rspec (3.13.0)
68 | rspec-core (~> 3.13.0)
69 | rspec-expectations (~> 3.13.0)
70 | rspec-mocks (~> 3.13.0)
71 | rspec-core (3.13.2)
72 | rspec-support (~> 3.13.0)
73 | rspec-expectations (3.13.3)
74 | diff-lcs (>= 1.2.0, < 2.0)
75 | rspec-support (~> 3.13.0)
76 | rspec-mocks (3.13.2)
77 | diff-lcs (>= 1.2.0, < 2.0)
78 | rspec-support (~> 3.13.0)
79 | rspec-support (3.13.1)
80 | ruby2_keywords (0.0.5)
81 | sentry-raven (3.1.2)
82 | faraday (>= 1.0)
83 | sinatra (4.0.0)
84 | mustermann (~> 3.0)
85 | rack (>= 3.0.0, < 4)
86 | rack-protection (= 4.0.0)
87 | rack-session (>= 2.0.0, < 3)
88 | tilt (~> 2.0)
89 | tilt (2.4.0)
90 | uri (1.0.1)
91 | webmock (3.24.0)
92 | addressable (>= 2.8.0)
93 | crack (>= 0.3.2)
94 | hashdiff (>= 0.4.0, < 2.0.0)
95 |
96 | PLATFORMS
97 | ruby
98 |
99 | DEPENDENCIES
100 | dotenv
101 | foreman
102 | httparty
103 | kramdown
104 | link_header
105 | nokogiri
106 | puma
107 | rack-contrib
108 | rack-ssl
109 | rack-test
110 | rackup
111 | redis
112 | rspec
113 | sentry-raven
114 | sinatra
115 | webmock
116 |
117 | RUBY VERSION
118 | ruby 3.3.5p100
119 |
120 | BUNDLED WITH
121 | 2.5.16
122 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Barry Frost
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: rackup -s puma -p $PORT
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Micropublish
2 |
3 | Micropublish is a [Micropub][] client that you can use to create, update,
4 | delete and undelete content on your Micropub-enabled site.
5 | A live install of Micropublish is running at [https://micropublish.net][mp]
6 |
7 |
10 |
11 | ---
12 |
13 | ## Features
14 |
15 | - Create, update, delete and undelete posts on your site.
16 | - Templates for creating Notes, Articles, RSVPs, Bookmarks, Replies, Reposts
17 | and Likes.
18 | - Use `x-www-form-urlencoded` (form-encoded) or JSON Micropub request methods.
19 | - Preview the request that will be sent to your server before it's sent.
20 | - Supports multiple values for URL properties (e.g. `in-reply-to[]`).
21 | - Customize types, icons, defaults, ordering and required properties.
22 | - Mobile-first. All views have been designed and optimized for mobile.
23 | - JavaScript is not required and you can happily use Micropublish without it.
24 | The user interface is progressively enhanced when JavaScript is enabled.
25 | - Full errors and feedback displayed from your endpoints.
26 | - Supports the `post-status` property
27 | [proposed as a Micropub extension][post-status].
28 |
29 | ---
30 |
31 | ## Requirements
32 |
33 | There are a number of requirements that your site's Micropub endpoint must meet
34 | in order to work with Micropublish.
35 |
36 | To learn more about setting up a Micropub
37 | endpoint, read the
38 | [Creating a Micropub endpoint][micropub-endpoint] page on the
39 | [IndieWeb wiki][indieweb] and the latest [Micropub specification][micropub].
40 |
41 | Below is what Micropublish expects from your server.
42 |
43 |
44 | ### Endpoint discovery
45 |
46 | When you enter your site's URL and click "Sign in" Micropublish will attempt to
47 | find three endpoints in either your site's HTTP response header or in its
48 | HTML `
#{url_property}
" +
47 | "accepts only one or more URLs separated by whitespace.")
48 | end
49 | end
50 | end
51 | end
52 | # check all required properties have been provided
53 | required.each do |property|
54 | if !@properties.key?(property) ||
55 | (property == 'checkin' &&
56 | (@properties['checkin'][0]['properties']['name'][0].empty? ||
57 | @properties['checkin'][0]['properties']['latitude'][0].empty? ||
58 | @properties['checkin'][0]['properties']['longitude'][0].empty?))
59 | raise MicropublishError.new('post',
60 | "#{property}
is required for the form to be " +
61 | "submitted. Please enter a value for this property.")
62 | end
63 | end
64 | end
65 |
66 | def h_type
67 | @type[0].gsub(/^h\-/,'')
68 | end
69 |
70 | def to_form_encoded
71 | props = Hash[@properties.map { |k,v| v.size > 1 ? ["#{k}[]", v] : [k,v] }]
72 | query = { h: h_type }.merge(props)
73 | URI.encode_www_form(query)
74 | end
75 |
76 | def to_json(pretty=false)
77 | hash = { type: @type, properties: @properties }
78 | pretty ? JSON.pretty_generate(hash) : hash.to_json
79 | end
80 |
81 | def diff_properties(submitted)
82 | diff = {
83 | replace: {},
84 | add: {},
85 | delete: []
86 | }
87 | diff_removed!(diff, submitted)
88 | diff_added!(diff, submitted)
89 | diff_replaced!(diff, submitted)
90 | diff
91 | end
92 |
93 | def diff_removed!(diff, submitted)
94 | @properties.keys.each do |prop|
95 | if !submitted.key?(prop) || submitted[prop].empty?
96 | diff[:delete] << prop
97 | end
98 | end
99 | diff.delete(:delete) if diff[:delete].empty?
100 | end
101 |
102 | def diff_added!(diff, submitted)
103 | submitted.keys.each do |prop|
104 | if !@properties.key?(prop)
105 | diff[:add][prop] = submitted[prop].is_a?(Array) ? submitted[prop] :
106 | [submitted[prop]]
107 | end
108 | end
109 | diff.delete(:add) if diff[:add].empty?
110 | end
111 |
112 | def diff_replaced!(diff, submitted)
113 | submitted.keys.each do |prop|
114 | if @properties.key?(prop) && @properties[prop] != submitted[prop]
115 | diff[:replace][prop] = submitted[prop].is_a?(Array) ? submitted[prop] :
116 | [submitted[prop]]
117 | end
118 | end
119 | diff.delete(:replace) if diff[:replace].empty?
120 | end
121 |
122 | def entry_type
123 | if @properties.key?('rsvp') &&
124 | %w(yes no maybe interested).include?(@properties['rsvp'][0])
125 | 'rsvp'
126 | elsif @properties.key?('in-reply-to') &&
127 | Auth.valid_uri?(@properties['in-reply-to'][0])
128 | 'reply'
129 | elsif @properties.key?('repost-of') &&
130 | Auth.valid_uri?(@properties['repost-of'][0])
131 | 'repost'
132 | elsif @properties.key?('like-of') &&
133 | Auth.valid_uri?(@properties['like-of'][0])
134 | 'like'
135 | elsif @properties.key?('bookmark-of') &&
136 | Auth.valid_uri?(@properties['bookmark-of'][0])
137 | 'bookmark'
138 | elsif @properties.key?('listen-of') &&
139 | Auth.valid_uri?(@properties['listen-of'][0])
140 | 'listen'
141 | elsif @properties.key?('photo') && @properties['photo'].is_a?(Array) &&
142 | @properties['photo'].size > 0
143 | 'photo'
144 | elsif @properties.key?('name') && !@properties['name'].empty? &&
145 | !content_start_with_name?
146 | 'article'
147 | elsif @properties.key?('checkin')
148 | 'checkin'
149 | else
150 | 'note'
151 | end
152 | end
153 |
154 | def content_start_with_name?
155 | return unless @properties.key?('content') && @properties.key?('name')
156 | content = @properties['content'][0].is_a?(Hash) &&
157 | @properties['content'][0].key?('html') ?
158 | @properties['content'][0]['html'] : @properties['content'][0]
159 | content_tidy = content.strip.gsub(/\s+/, " ")
160 | name_tidy = @properties['name'][0].strip.gsub(/\s+/, " ")
161 | content_tidy.start_with?(name_tidy)
162 | end
163 |
164 | private
165 |
166 | def self.remove_trix_attributes(hash)
167 | return hash unless hash.has_key?("content") && hash["content"]&.first["html"]
168 |
169 | doc = Nokogiri::HTML5.fragment(hash["content"]&.first["html"])
170 |
171 | doc.css('figure[data-trix-attachment]').each do |attachment|
172 | image_src = attachment.at_css('img')['src'] if attachment.at_css('img')
173 | figcaption = attachment.at_css('figcaption').content.strip if attachment.at_css('figcaption')
174 |
175 | if image_src
176 | img_tag = Nokogiri::XML::Node.new('img', doc)
177 | img_tag['src'] = image_src
178 | img_tag['alt'] = figcaption if figcaption
179 |
180 | attachment.replace(img_tag)
181 | end
182 | end
183 |
184 | hash["content"]&.first["html"] = doc.to_html
185 |
186 | hash
187 | end
188 |
189 | end
190 | end
191 |
--------------------------------------------------------------------------------
/lib/micropublish/request.rb:
--------------------------------------------------------------------------------
1 | module Micropublish
2 | class Request
3 |
4 | def initialize(micropub, token, is_json=false)
5 | @micropub = micropub
6 | @token = token
7 | @is_json = is_json
8 | end
9 |
10 | def create(post)
11 | body = if @is_json
12 | { type: post.type, properties: post.properties }
13 | else
14 | # flatten single value arrays
15 | { h: post.h_type }.merge(
16 | Hash[post.properties.map { |k,v| [k, v.size == 1 ? v[0] : v] }]
17 | )
18 | end
19 | response = send(body)
20 | case response.code.to_i
21 | when 201, 202
22 | response.headers['location']
23 | else
24 | handle_error(response.body)
25 | end
26 | end
27 |
28 | def update(url, diff, mp_commands)
29 | body = { action: 'update', url: url }.merge(diff).merge(mp_commands)
30 | response = send(body, true)
31 | case response.code.to_i
32 | when 201
33 | response.headers['location']
34 | when 200, 204
35 | url
36 | else
37 | handle_error(response.body)
38 | end
39 | end
40 |
41 | def delete(url)
42 | body = { action: 'delete', url: url }
43 | response = send(body)
44 | case response.code.to_i
45 | when 200, 204
46 | url
47 | else
48 | handle_error(response.body)
49 | end
50 | end
51 |
52 | def undelete(url)
53 | body = { action: 'undelete', url: url }
54 | response = send(body)
55 | case response.code.to_i
56 | when 201
57 | response.headers['location']
58 | when 200, 204
59 | url
60 | else
61 | handle_error(response.body)
62 | end
63 | end
64 |
65 | def upload(file)
66 | response = HTTParty.post(
67 | @micropub,
68 | body: {
69 | file: file
70 | },
71 | headers: { 'Authorization' => "Bearer #{@token}" }
72 | )
73 |
74 | case response.code.to_i
75 | when 201
76 | response.headers['location']
77 | else
78 | handle_error(response.body)
79 | end
80 | end
81 |
82 | private
83 |
84 | def send(body, is_json=@is_json)
85 | headers = { 'Authorization' => "Bearer #{@token}" }
86 | if is_json
87 | body = body.to_json
88 | headers['Content-Type'] = 'application/json; charset=utf-8'
89 | end
90 | HTTParty.post(
91 | @micropub,
92 | body: body,
93 | headers: headers
94 | )
95 | end
96 |
97 | def handle_error(response_body)
98 | raise MicropublishError.new('request',
99 | "There was an error making a request to your Micropub endpoint. " +
100 | "The error received was: #{response_body}")
101 | end
102 |
103 | end
104 | end
--------------------------------------------------------------------------------
/lib/micropublish/server.rb:
--------------------------------------------------------------------------------
1 | require 'securerandom'
2 | require 'base64'
3 | require 'digest'
4 |
5 | module Micropublish
6 | class Server < Sinatra::Application
7 |
8 | configure do
9 | helpers Helpers
10 |
11 | use Rack::SSL if ENV['FORCE_SSL'] == '1'
12 |
13 | root_path = "#{File.dirname(__FILE__)}/../../"
14 | set :views, "#{root_path}views"
15 | set :public_folder, "#{root_path}public"
16 | set :properties,
17 | JSON.parse(File.read("#{root_path}config/properties.json"))
18 | set :readme, File.read("#{root_path}README.md")
19 | set :changelog, File.read("#{root_path}/changelog.md")
20 | set :help, File.read("#{public_folder}/help.md")
21 |
22 | set :server, :puma
23 |
24 | # use a cookie that lasts for 30 days
25 | secret = ENV['COOKIE_SECRET'] || SecureRandom.hex(64)
26 | use Rack::Session::Cookie, secret: secret, expire_after: 2_592_000
27 | end
28 |
29 | before do
30 | unless settings.production?
31 | session[:me] = ENV['DEV_ME'] || 'http://localhost:4444/'
32 | session[:micropub] = ENV['DEV_MICROPUB'] || 'http://localhost:3333/micropub'
33 | session[:scope] = ENV['DEV_SCOPE'] || 'create update delete undelete'
34 | session[:token] = ENV['DEV_TOKEN'] || nil
35 | end
36 | end
37 |
38 | get '/' do
39 | if logged_in?
40 | @title = "Dashboard"
41 | @types = post_types
42 | erb :dashboard
43 | else
44 | @title = "Sign in"
45 | @about = markdown(settings.readme)
46 | erb :login
47 | end
48 | end
49 |
50 | get '/auth' do
51 | begin
52 | unless params.key?('me') && !params[:me].empty? &&
53 | Auth.valid_uri?(params[:me])
54 | raise "Missing or invalid value for \"me\": \"#{h params[:me]}\"."
55 | end
56 | unless params.key?('scope') && (
57 | params[:scope].include?('create') ||
58 | params[:scope].include?('post') ||
59 | params[:scope].include?('draft'))
60 | raise "You must specify a valid scope, including at least one of " +
61 | "\"create\", \"post\" or \"draft\"."
62 | end
63 | unless endpoints = EndpointsFinder.new(params[:me]).find_links
64 | raise "Client could not find expected endpoints at \"#{h params[:me]}\"."
65 | end
66 | rescue => e
67 | redirect_flash('/', 'danger', e.message)
68 | end
69 | # define random state string
70 | session[:state] = SecureRandom.alphanumeric(20)
71 | # store scope - will be needed to limit functionality on dashboard
72 | session[:scope] = params[:scope].join(' ')
73 | # store me - we don't want to trust this in callback
74 | session[:me] = params[:me]
75 | # code challenge from code verified
76 | session[:code_verifier] = SecureRandom.alphanumeric(100)
77 | code_challenge = Auth.generate_code_challenge(session[:code_verifier])
78 | # redirect to auth endpoint
79 | query = URI.encode_www_form({
80 | me: session[:me],
81 | client_id: request.base_url + "/",
82 | state: session[:state],
83 | scope: session[:scope],
84 | redirect_uri: "#{request.base_url}/auth/callback",
85 | response_type: "code",
86 | code_challenge: code_challenge,
87 | code_challenge_method: "S256"
88 | })
89 | redirect "#{endpoints[:authorization_endpoint]}?#{query}"
90 | end
91 |
92 | get '/auth/callback' do
93 | unless session.key?(:me) && session.key?(:state) && session.key?(:scope)
94 | redirect_flash('/', 'info', "Session has timed out. Please try again.")
95 | end
96 | unless params.key?(:state) && params[:state] == session[:state]
97 | session.clear
98 | redirect_flash('/', 'info', "Callback \"state\" parameter is missing or does not match.")
99 | end
100 | auth = Auth.new(
101 | session[:me],
102 | params[:code],
103 | "#{request.base_url}/auth/callback",
104 | request.base_url + "/",
105 | session[:code_verifier]
106 | )
107 | endpoints_and_token_and_scope_and_me = auth.callback
108 | # login and token grant was successful so store in session with the scope for the token and the me
109 | session.merge!(endpoints_and_token_and_scope_and_me)
110 | redirect_flash('/', 'success', %Q{You are now signed in successfully
111 | as "#{endpoints_and_token_and_scope_and_me[:me]}".
112 | Submit content to your site via Micropub using the links
113 | below. Please
114 | read the docs for
115 | more information and help.})
116 | end
117 |
118 | get '/new' do
119 | redirect '/new/h-entry/note'
120 | end
121 | get '/new/h-entry' do
122 | redirect '/new/h-entry/note'
123 | end
124 |
125 | get %r{/new/h\-entry/(note|article|bookmark|reply|repost|like|rsvp|checkin|photo|listen|food|drink)} do
126 | |subtype|
127 | require_session
128 | render_new(subtype)
129 | end
130 |
131 | post '/new' do
132 | require_session
133 | begin
134 | @post = Post.new([params[:_type]], Post.properties_from_params(params))
135 | required_properties = post_types[params[:_subtype]]['required']
136 | @post.validate_properties!(required_properties)
137 | # articles must be sent as json because content is an object
138 | format = params[:_subtype] == 'article' ? :json : default_format
139 | if params.key?('_preview')
140 | if format == :json
141 | @content = h @post.to_json(true)
142 | else
143 | @content = h format_form_encoded(@post.to_form_encoded)
144 | end
145 | if request.xhr?
146 | @content
147 | else
148 | erb :preview
149 | end
150 | else
151 | url = new_request.create(@post)
152 | redirect_post(url)
153 | end
154 | rescue MicropublishError => e
155 | if request.xhr?
156 | status 500
157 | e.message
158 | else
159 | set_flash('danger', e.message)
160 | render_new(params[:_subtype])
161 | end
162 | end
163 | end
164 |
165 | get '/edit' do
166 | require_session
167 | redirect "/delete?url=#{params[:url]}" if params.key?('delete')
168 | redirect "/undelete?url=#{params[:url]}" if params.key?('undelete')
169 | redirect "/edit-all?url=#{params[:url]}" if params.key?('edit-all')
170 |
171 | subtype = micropub.source_all(params[:url]).entry_type
172 | if post_types.key?(subtype)
173 | render_edit(subtype)
174 | else
175 | render_edit_all
176 | end
177 | end
178 |
179 | get %r{/edit/h\-entry/(note|article|bookmark|reply|repost|like|rsvp|checkin|photo|listen|food|drink)} do
180 | |subtype|
181 | require_session
182 | render_edit(subtype)
183 | end
184 |
185 | get '/edit-all' do
186 | require_session
187 | render_edit_all
188 | end
189 |
190 | post '/edit' do
191 | require_session
192 | begin
193 | subtype = params[:_subtype]
194 | submitted_properties = Post.properties_from_params(params)
195 | @post = Post.new([params[:_type]], submitted_properties)
196 | @post.validate_properties!
197 | original_properties = if params.key?('_all')
198 | micropub.source_all(params[:_url]).properties
199 | else
200 | micropub.source_properties(params[:_url],
201 | subtype_edit_properties(subtype)).properties
202 | end
203 | mp_commands = Micropub.find_commands(params)
204 | known_properties = post_types.key?(subtype) ?
205 | settings.properties['known'].select{ |p| (post_types[subtype]['properties'] + %w(syndication published)).any?(p) } :
206 | settings.properties['known']
207 | diff = Compare.new(original_properties, submitted_properties,
208 | known_properties).diff_properties
209 | if params.key?('_preview')
210 | hash = {
211 | action: 'update',
212 | url: params[:_url]
213 | }.merge(diff).merge(mp_commands)
214 | @content = h JSON.pretty_generate(hash)
215 | if request.xhr?
216 | @content
217 | else
218 | erb :preview
219 | end
220 | else
221 | url = new_request.update(params[:_url], diff, mp_commands)
222 | redirect_post(url)
223 | end
224 | rescue MicropublishError => e
225 | if request.xhr?
226 | status 500
227 | e.message
228 | else
229 | set_flash('danger', e.message)
230 | params.key?('_all') ? render_edit_all : render_edit(params[:_subtype])
231 | end
232 | end
233 | end
234 |
235 | get '/delete' do
236 | require_session
237 | require_url
238 | @title = "Delete post at #{params[:url]}"
239 | erb :delete
240 | end
241 |
242 | post '/delete' do
243 | require_session
244 | url = new_request.delete(params[:url])
245 | redirect params[:url]
246 | end
247 |
248 | get '/undelete' do
249 | require_session
250 | require_url
251 | @title = "Undelete post at #{params[:url]}"
252 | erb :undelete
253 | end
254 |
255 | post '/undelete' do
256 | require_session
257 | url = new_request.undelete(params[:url])
258 | redirect params[:url]
259 | end
260 |
261 | post '/settings' do
262 | if params.key?('format') && ['json','form'].include?(params[:format])
263 | session[:format] = params[:format].to_sym
264 | format_label = params[:format] == 'json' ? 'JSON' : 'form-encoded'
265 | session[:flash] = {
266 | type: 'info',
267 | message: "Format setting updated to \"#{format_label}\". New posts " +
268 | "will be sent using #{format_label} format."
269 | }
270 | end
271 | redirect '/'
272 | end
273 |
274 | post '/logout' do
275 | logout!
276 | end
277 |
278 | get '/about' do
279 | @content = markdown(settings.readme)
280 | # use a better heading for the about page
281 | @content.sub!('" + data + ""; 12 | $('#preview-content').html(content); 13 | }, 14 | error: function(xhr, desc, error) { 15 | var content = "
2 | Create or modify posts on your 3 | Micropub-compatible website. 4 | Find out more about the requirements. 5 |
6 | 7 |<%= @content %>7 |