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:
parent
fcf3eb14d0
commit
9e96f29852
5 changed files with 227 additions and 0 deletions
|
@ -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
|
||||||
|
|
75
src/actions/articles/show.cr
Normal file
75
src/actions/articles/show.cr
Normal 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
|
20
src/actions/clients/local_client.cr
Normal file
20
src/actions/clients/local_client.cr
Normal 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
|
90
src/actions/clients/medium_client.cr
Normal file
90
src/actions/clients/medium_client.cr
Normal 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
|
38
src/pages/articles/show_page.cr
Normal file
38
src/pages/articles/show_page.cr
Normal 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
|
Loading…
Reference in a new issue