diff --git a/config/env.cr b/config/env.cr index 5bb7ad0..d2d07e7 100644 --- a/config/env.cr +++ b/config/env.cr @@ -17,4 +17,8 @@ module Lucky::Env def task? ENV["LUCKY_TASK"]? == "true" end + + def use_local? + ENV.fetch("USE_LOCAL", "false") == "true" + end end diff --git a/src/actions/articles/show.cr b/src/actions/articles/show.cr new file mode 100644 index 0000000..7f10779 --- /dev/null +++ b/src/actions/articles/show.cr @@ -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 diff --git a/src/actions/clients/local_client.cr b/src/actions/clients/local_client.cr new file mode 100644 index 0000000..f9a2463 --- /dev/null +++ b/src/actions/clients/local_client.cr @@ -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 diff --git a/src/actions/clients/medium_client.cr b/src/actions/clients/medium_client.cr new file mode 100644 index 0000000..aee8ad4 --- /dev/null +++ b/src/actions/clients/medium_client.cr @@ -0,0 +1,90 @@ +require "json" + +class MediumClient + # https://stackoverflow.com/questions/2669690/ + JSON_HIJACK_STRING = "])}while(1);" + + 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 diff --git a/src/pages/articles/show_page.cr b/src/pages/articles/show_page.cr new file mode 100644 index 0000000..ef59992 --- /dev/null +++ b/src/pages/articles/show_page.cr @@ -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