Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ gem 'puma', '~> 7.2.1'
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'

# used by lib/link_preview.rb
gem 'metainspector', '~> 5.15'
gem 'microformats', '~> 4.5'

# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'mini_racer', platforms: :ruby

Expand All @@ -52,8 +57,6 @@ gem 'jbuilder', '~> 2.5'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

#gem 'aws-sdk-rails', '~> 2.1.0' #email with AWS SES

group :production do
gem "aws-sdk-s3", require: false
end
Expand Down
47 changes: 47 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
domain_name (0.6.20240107)
dotenv (2.7.6)
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
Expand All @@ -143,12 +144,35 @@ GEM
erb (6.0.1.1)
erubi (1.13.1)
execjs (2.10.0)
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
http-cookie (>= 1.0.0)
faraday-encoding (0.0.6)
faraday
faraday-follow_redirects (0.5.0)
faraday (>= 1, < 3)
faraday-gzip (3.1.0)
faraday (>= 2.0, < 3)
zlib (~> 3.0)
faraday-http-cache (2.7.0)
faraday (>= 0.8)
faraday-net_http (3.4.4)
net-http (~> 0.5)
faraday-retry (2.4.0)
faraday (~> 2.0)
fastimage (2.4.1)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
globalid (1.2.1)
activesupport (>= 6.1)
http-cookie (1.1.6)
domain_name (~> 0.5)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
Expand All @@ -163,6 +187,7 @@ GEM
actionview (>= 7.0.0)
activesupport (>= 7.0.0)
jmespath (1.6.2)
json (2.20.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
Expand Down Expand Up @@ -193,11 +218,29 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.3)
metainspector (5.17.2)
addressable (~> 2.9.0)
faraday (~> 2.5)
faraday-cookie_jar (~> 0.0)
faraday-encoding (~> 0.0)
faraday-follow_redirects (~> 0.3)
faraday-gzip (>= 0.1, < 4.0)
faraday-http-cache (~> 2.5)
faraday-retry (~> 2.0)
fastimage (~> 2.2)
nesty (~> 1.0)
nokogiri (~> 1.19.0)
microformats (4.5.0)
json
nokogiri
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.5)
msgpack (1.8.0)
mutex_m (0.3.0)
nesty (1.0.2)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.5.15)
date
net-protocol
Expand Down Expand Up @@ -325,6 +368,7 @@ GEM
concurrent-ruby (~> 1.0)
uglifier (4.2.1)
execjs (>= 0.3.0, < 3)
uri (1.1.1)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
Expand All @@ -341,6 +385,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.3)
zlib (3.2.3)

PLATFORMS
aarch64-linux
Expand All @@ -364,6 +409,8 @@ DEPENDENCIES
kaminari (~> 1.2.1)
letter_opener (~> 1.7.0)
listen (>= 3.0.5, < 3.2)
metainspector (~> 5.15)
microformats (~> 4.5)
pg (~> 1.5.9)
puma (~> 7.2.1)
rails (~> 7.1.5.2)
Expand Down
29 changes: 29 additions & 0 deletions app/assets/stylesheets/misc.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,32 @@ details.recent_activity summary h2:before {
span.reaction_age {
font-size: small;
}

.link-preview-card {
display: block;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
text-decoration: none;
color: inherit;
max-width: 70%;
margin: 1rem 0;
}
.preview-content {
padding: 1rem;
}
.preview-title {
display: block;
font-size: 1.2rem;
margin-bottom: 0.5rem;
}
.preview-description {
font-size: 0.8rem;
color: #555;
margin: 0;
line-height: 1.4;
}
.preview-image {
max-width: 100%;
height: auto;
}
46 changes: 42 additions & 4 deletions app/controllers/micropub_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def query
"name": "Post",
"properties": [ "content", "published" ]
},
{
"type": "photo",
"name": "Image"
}
]
}, status: 200
elsif params[:q] == "source"
Expand Down Expand Up @@ -158,24 +162,58 @@ def post_from_url(params_url)

# supported params: name, content, published
def post_from_params(params)
unless params[:h] and params[:h]=="entry"
if params[:h] and params[:h]=="entry"
# we handle this case
elsif params[:type] and params[:type].first=="h-entry"
raise JsonError.new("invalid_request","JSON params not supported by this server",400)
else
raise JsonError.new("invalid_request","Only h-entry types supported by this server",400)
end

content = ""
if params[:content]
if params["bookmark-of"]
content_array = []
if params[:name]
content_array << "## Bookmark: #{params[:name]}"
else
content_array << "## Bookmark"
end
content_array << params[:content] if params[:content]
link_preview = LinkPreview.fetch(params["bookmark-of"])
content_array << LinkPreview.render(params["bookmark-of"],link_preview)
content = content_array.join("\n\n")
elsif params[:content]
content = params[:content]
if params[:name]
content = "# #{params[:name]}\n\n#{content}"
end
elsif params[:name]
content = "# #{params[:name]}"
content = "## #{params[:name]}"
elsif params["like-of"]
link_preview = LinkPreview.fetch(params["like-of"])
content = "<p>❤️ Liked <a class=\"u-like-of\" href=\"#{params['like-of']}\">#{link_preview[:title]}</a></p>"
content += LinkPreview.render(params['like-of'],link_preview)
elsif params["repost-of"]
link_preview = LinkPreview.fetch(params["repost-of"])
content = "<p>🔁 Reposted: <a class=\"u-repost-of\" href=\"#{params['repost-of']}\">#{link_preview[:title]}</a></p>"
content += LinkPreview.render(params['repost-of'],link_preview)
else
raise JsonError.new("invalid_request","New h-entry must have content or a name (or both)",400)
raise JsonError.new("invalid_request","New h-entry must have content or a name (or both), or have a repost-of or like-of",400)
end
datetime = DateTime.now
if params[:published]
datetime = DateTime.parse(params[:published])
end

## these blocks insert in-reply-to or bookmark-of (etc..) links directly in the content
## we might consider richer database structure to store/display that data better.
if params["in-reply-to"]
link_preview = LinkPreview.fetch(params["in-reply-to"])
reply_html = "In reply to: <a class=\"u-in-reply-to\" href=\"#{params["in-reply-to"]}\">#{link_preview[:title]}</a>"
content += "\n\n#{reply_html}"
content += "\n\n#{LinkPreview.render(params["in-reply-to"], link_preview)}"
end

post = Post.new
post.datetime = datetime
post.content = content
Expand Down
3 changes: 2 additions & 1 deletion lib/indie_auth_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ def to_html

def valid_redirect? (redirect)
return true if redirect.start_with? @client_id
return true if URI.parse(redirect).origin == URI.parse(@client_id).origin
return true if @redirects.include? redirect
#puts "redirect validation error. Indie auth client #{@client_id} requesting redirect to #{redirect} which does not have #{@client_id} as a prefix, and is not in list of valid redirects fetched: #{@redirects.join(",")}"
# puts "redirect validation error. Indie auth client #{@client_id} requesting redirect to #{redirect} which does not have #{@client_id} as a prefix, and is not in list of valid redirects fetched: [#{@redirects.join(",")}]"
return false
end

Expand Down
87 changes: 87 additions & 0 deletions lib/link_preview.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@

module LinkPreview
require 'open-uri'
require 'nokogiri'
require 'microformats'
require 'metainspector'
require 'cgi'

def self.fetch(url)
begin
uri = URI.parse(url)
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
raise ArgumentError, "Only HTTP/HTTPS URLs are allowed"
end
html = uri.open.read
rescue StandardError => e
puts "Failed to fetch URL: #{e.message}"
return nil
end

doc = Nokogiri::HTML(html)

# Check for standard mf2 root classes
if false # mf2 parsing isn't working well TODO: investigate further
# if doc.at_css('.h-entry, .h-card, .h-event')
# puts "mf2 detected! Parsing with microformats-ruby..."
parsed = Microformats.parse(html)
entry = parsed.items.first
raw_description = entry.properties['summary']&.first || entry.properties['content']&.first
clean_description = if raw_description.is_a?(Hash)
# Handle both string and symbol keys just in case
raw_description['value'] || raw_description[:value] || ""
else
raw_description
end
return {
title: entry.properties['name']&.first,
description: clean_description.truncate(250, separator: ' '),
image: entry.properties['photo']&.first,
type: 'mf2'
}

else
# puts "No mf2 found. Falling back to MetaInspector..."
page = MetaInspector.new(url, document: html)

return {
title: page.best_title,
description: page.best_description,
image: page.images.best,
type: 'opengraph'
}
end
end


def self.render(url, preview)
# Fallback to a standard link if the preview hash is nil
return %Q(<a href="#{url}">#{url}</a>) if preview.nil?

# Safely escape text to prevent XSS injection
title = CGI.escapeHTML(preview[:title] || url)
description = CGI.escapeHTML(preview[:description] || "")

# Conditionally render the image tag only if an image exists
image_html = if preview[:image]
%Q(<img src="#{preview[:image]}" alt="Preview for #{title}" class="preview-image">)
else
""
end
description_html = description.empty? ? "" : %Q(<p class="preview-description">#{description}</p>)


<<~HTML
<a href="#{url}" class="link-preview-card">
#{image_html}
<div class="preview-content">
<strong class="preview-title">
#{title}
</strong>
#{description_html}
</div>
</a>
HTML
end

end # Module
Loading