Add basic response (except images)

The basic idea here is to fetch the post with the medium API, parse the
JSON into types, and then re-display the content. We also have to fetch
each media object as a REST call to get things like embeded iframes.
This commit is contained in:
Edward Loveall 2021-05-01 17:26:48 -04:00
parent fcf3eb14d0
commit 9e96f29852
No known key found for this signature in database
GPG key ID: 789A4AE983AC8901
5 changed files with 227 additions and 0 deletions

View file

@ -17,4 +17,8 @@ module Lucky::Env
def task? def task?
ENV["LUCKY_TASK"]? == "true" ENV["LUCKY_TASK"]? == "true"
end end
def use_local?
ENV.fetch("USE_LOCAL", "false") == "true"
end
end end

View file

@ -0,0 +1,75 @@
require "json"
class Articles::Show < BrowserAction
get "/post/:post_id" do
if Lucky::Env.use_local?
response = LocalClient.post_data(post_id)
else
response = MediumClient.post_data(post_id)
end
html ShowPage, medium_response_body: PostResponse::Root.from_json(response.body)
end
end
class PostResponse
class Base
include JSON::Serializable
end
class Root < Base
property data : Data
end
class Data < Base
property post : Post
end
class Post < Base
property title : String
property creator : Creator
property content : Content
end
class Creator < Base
property name : String
property id : String
end
class Content < Base
property bodyModel : BodyModel
end
class BodyModel < Base
property paragraphs : Array(Paragraph)
end
class Paragraph < Base
property text : String
property type : ParagraphType
property iframe : IFrame?
property layout : String?
end
enum ParagraphType
H3
H4
P
PRE
BQ
ULI
OLI
IFRAME
IMG
end
class IFrame < Base
property mediaResource : MediaResource
end
class MediaResource < Base
property id : String
end
class Metadata < Base
end
end

View file

@ -0,0 +1,20 @@
require "./medium_client"
# This allows you to read posts responses from a local file instead of hitting # the API all the time. You can get an api response by inserting the post id
# in this curl(1) command:
# curl -X "POST" "https://medium.com/_/graphql" \
# -H 'Content-Type: application/json; charset=utf-8' \
# -d $'{
# "query": "query{post(id:\\"[post id here]\\"){title creator{name id}content{bodyModel{paragraphs{text type markups{name title type href start end rel anchorType}href iframe{mediaResource{id}}layout metadata{__typename id originalWidth originalHeight}}}}}}"
# }' > [post id here].json
# Then place it in the /tmp/posts directory. The post id will come in as a
# query param and go look for a file with a matching filename.
class LocalClient < MediumClient
def self.post_data(post_id : String) : HTTP::Client::Response
body = File.read("tmp/posts/#{post_id}.json")
HTTP::Client::Response.new(HTTP::Status::OK, body: body)
end
end

View file

@ -0,0 +1,90 @@
require "json"
class MediumClient
# https://stackoverflow.com/questions/2669690/
JSON_HIJACK_STRING = "])}while(1);</x>"
def self.post_data(post_id : String) : HTTP::Client::Response
client = HTTP::Client.new("medium.com", tls: true)
client.post("/_/graphql", headers: headers, body: body(post_id))
end
def self.embed_data(media_id : String) : MediaResponse::Root
client = HTTP::Client.new("medium.com", tls: true)
response = client.get("/media/#{media_id}", headers: headers)
body = response.body.sub(JSON_HIJACK_STRING, nil)
MediaResponse::Root.from_json(body)
end
private def self.headers : HTTP::Headers
HTTP::Headers{
"Accept" => "application/json",
"Content-Type" => "application/json; charset=utf-8",
}
end
private def self.body(post_id : String) : String
query = <<-GRAPHQL
query {
post(id: "#{post_id}") {
title
creator {
id
name
}
content {
bodyModel {
paragraphs {
text
type
markups {
name
type
start
end
}
href
iframe {
mediaResource {
id
}
}
metadata {
__typename
id
originalWidth
originalHeight
}
}
}
}
}
}
GRAPHQL
JSON.build do |json|
json.object do
json.field "query", query
json.field "variables", {} of String => String
end
end
end
end
class MediaResponse
class Base
include JSON::Serializable
end
class Root < Base
property payload : Payload
end
class Payload < Base
property value : Value
end
class Value < Base
property href : String
property iframeSrc : String
end
end

View file

@ -0,0 +1,38 @@
class Articles::ShowPage < MainLayout
needs medium_response_body : PostResponse::Root
def content
paragraphs = medium_response_body.data.post.content.bodyModel.paragraphs
paragraphs.each do |paragraph|
case paragraph.type
when PostResponse::ParagraphType::H3
h3 paragraph.text
when PostResponse::ParagraphType::H4
h4 paragraph.text
when PostResponse::ParagraphType::P
para paragraph.text
when PostResponse::ParagraphType::PRE
pre paragraph.text
when PostResponse::ParagraphType::BQ
blockquote paragraph.text
when PostResponse::ParagraphType::OLI
li paragraph.text
when PostResponse::ParagraphType::ULI
li paragraph.text
when PostResponse::ParagraphType::IFRAME
embed = paragraph.iframe
if embed
embed_data = LocalClient.embed_data(embed.mediaResource.id)
embed_value = embed_data.payload.value
if embed_value.iframeSrc.blank?
iframe src: embed_data.payload.value.href
else
iframe src: embed_data.payload.value.iframeSrc
end
end
else
para "#{paragraph.type} not yet implimented"
end
end
end
end