scribe/src/clients/medium_client.cr

70 lines
1.6 KiB
Crystal
Raw Normal View History

require "json"
class MediumClient
2021-05-15 23:05:28 +02:00
def self.post_data(post_id : String) : PostResponse::Root
client = HTTP::Client.new("medium.com", tls: true)
2021-05-15 23:05:28 +02:00
response = client.post("/_/graphql", headers: headers, body: body(post_id))
PostResponse::Root.from_json(response.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
2021-09-04 23:32:27 +02:00
createdAt
creator {
id
name
}
content {
bodyModel {
paragraphs {
text
type
2021-08-29 19:02:56 +02:00
href
layout
markups {
2021-08-29 19:02:56 +02:00
title
type
href
2021-07-04 23:37:45 +02:00
userId
start
end
2021-08-29 19:02:56 +02:00
anchorType
}
iframe {
mediaResource {
href
Render embedded content PostResponse::Paragraph's that are of type IFRAME have extra data in the iframe attribute to specify what's in the iframe. Not all data is the same, however. I've identified three types and am using the new EmbeddedConverter class to convert them: * EmbeddedContent, the full iframe experience * GithubGist, because medium or github treat embeds differently for whatever reason * EmbeddedLink, the old style, just a link to the content. Effectively a fallback The size of the original iframe is also specified as an attribute. This code resizes it. The resizing is determined by figuring out the width/height ratio and setting the width to 800. EmbeddedContent can be displayed if we have an embed.ly url, which most iframe response data has. GitHub gists are a notable exception. Gists instead can be embedded simply by taking the gist URL and attaching .js to the end. That becomes the iframe's src attribute. The PostResponse::Paragraph's iframe attribute is nillable. Previous code used lots of if-statements with variable bindings to work with the possible nil values: ```crystal if foo = obj.nillable_value # obj.nillable_value was not nil and foo contains the value else # obj.nillable_value was nil so do something else end ``` See https://crystal-lang.org/reference/syntax_and_semantics/if_var.html for more info In the EmbeddedConverter the monads library has been introduced to get rid of at least one level of nillability. This wraps values in Maybe which allows for a cleaner interface: ```crystal Monads::Try(Value).new(->{ obj.nillable_value }) .to_maybe .fmap(->(value: Value) { # do something with value }) .value_or(# value was nil, do something else) ``` This worked to get the iframe attribute from a Paragraph: ```crystal Monads::Try(PostResponse::IFrame).new(->{ paragraph.iframe }) .to_maybe .fmap(->(iframe : PostResponse::IFrame) { # iframe is not nil! }) .fmap(#and so on) .value_or(Empty.new) ``` iframe only has one attribute: mediaResource which contains the iframe data. That was used to determine one of the three types above. Finally, Tufte.css has options for iframes. They mostly look good except for tweets which are too small and weirdly in the center of the page which actually looks off-center. That's for another day though.
2021-09-13 19:27:52 +02:00
iframeSrc
iframeWidth
iframeHeight
}
}
metadata {
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