Skip to content

Commit b2f121f

Browse files
committed
Implement GraphGL Client for Hackers' Pub
1 parent d303c81 commit b2f121f

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

plugins/utils/hackerspub/client.rb

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
require 'json'
2+
require 'net/http'
3+
require 'cgi'
4+
require 'time'
5+
require 'uri'
6+
7+
module Utils
8+
module Hackerspub
9+
class Error < StandardError; end
10+
11+
class Client
12+
DEFAULT_ENDPOINT = 'https://hackers.pub/graphql'
13+
14+
ARTICLES_QUERY = <<~GRAPHQL
15+
query Articles($handle: String!, $allowLocalHandle: Boolean!) {
16+
actorByHandle(handle: $handle, allowLocalHandle: $allowLocalHandle) {
17+
articles {
18+
edges {
19+
node {
20+
id
21+
language
22+
name
23+
published
24+
summary
25+
content
26+
url
27+
visibility
28+
}
29+
}
30+
}
31+
}
32+
}
33+
GRAPHQL
34+
35+
ACTOR_BIO_QUERY = <<~GRAPHQL
36+
query ActorBio($handle: String!, $allowLocalHandle: Boolean!) {
37+
actorByHandle(handle: $handle, allowLocalHandle: $allowLocalHandle) {
38+
account {
39+
bio
40+
avatarUrl
41+
}
42+
}
43+
}
44+
GRAPHQL
45+
46+
def initialize(handle:, endpoint: DEFAULT_ENDPOINT, base_url: 'https://hackers.pub')
47+
@handle = handle
48+
@endpoint = URI.parse(endpoint)
49+
@base_url = base_url
50+
end
51+
52+
def fetch_posts
53+
data = execute(ARTICLES_QUERY)
54+
edges = data.dig('actorByHandle', 'articles', 'edges') || []
55+
edges.filter_map { |edge| normalize_post(edge['node']) }
56+
end
57+
58+
def fetch_actor_bio
59+
data = execute(ACTOR_BIO_QUERY)
60+
data['actorByHandle']&.fetch('account', nil)
61+
end
62+
63+
private
64+
65+
attr_reader :handle, :endpoint, :base_url
66+
67+
def execute(query)
68+
payload = {
69+
query: query,
70+
variables: {
71+
handle: handle,
72+
allowLocalHandle: true
73+
}
74+
}
75+
76+
response = post_json(payload)
77+
parsed = JSON.parse(response.body)
78+
79+
if parsed['errors']&.any?
80+
message = parsed['errors'].map { |error| error['message'] }.join(', ')
81+
raise Error, "Hackerspub GraphQL error: #{message}"
82+
end
83+
84+
parsed['data']
85+
rescue JSON::ParserError => e
86+
raise Error, "Unable to parse Hackerspub response: #{e.message}"
87+
end
88+
89+
def post_json(payload)
90+
http = Net::HTTP.new(endpoint.host, endpoint.port)
91+
http.use_ssl = endpoint.scheme == 'https'
92+
93+
request = Net::HTTP::Post.new(endpoint)
94+
request['Content-Type'] = 'application/json'
95+
request.body = JSON.dump(payload)
96+
97+
response = http.request(request)
98+
unless response.is_a?(Net::HTTPSuccess)
99+
raise Error,
100+
"Hackerspub request failed (#{response.code} #{response.message})"
101+
end
102+
103+
response
104+
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
105+
raise Error, "Unable to connect to Hackerspub API: #{e.message}"
106+
end
107+
108+
def normalize_post(node) # rubocop:disable Metrics/AbcSize
109+
return unless node
110+
111+
url = node['url']
112+
return unless url.is_a?(String)
113+
114+
profile_base = profile_url_base
115+
return unless url.start_with?(profile_base)
116+
117+
path_segments = URI.parse(url).path.split('/').reject(&:empty?)
118+
return unless path_segments.length >= 3
119+
120+
year = path_segments[-2]
121+
encoded_slug = path_segments[-1]
122+
slug = CGI.unescape(encoded_slug)
123+
124+
{
125+
id: node['id'],
126+
name: node['name'],
127+
summary: node['summary'],
128+
content: node['content'],
129+
url: url,
130+
language: node['language'],
131+
visibility: node['visibility'],
132+
year: year,
133+
slug: slug,
134+
encoded_slug: encoded_slug,
135+
published_at: parse_time(node['published']),
136+
published_raw: node['published']
137+
}
138+
rescue URI::InvalidURIError
139+
nil
140+
end
141+
142+
def parse_time(value)
143+
return if value.nil?
144+
145+
case value
146+
when Time
147+
value
148+
when DateTime
149+
value.to_time
150+
when String
151+
Time.parse(value)
152+
end
153+
rescue ArgumentError
154+
nil
155+
end
156+
157+
def profile_url_base
158+
@profile_url_base ||= begin
159+
normalized_base = base_url.to_s.chomp('/')
160+
normalized_handle = handle.to_s
161+
normalized_handle = "@#{normalized_handle.delete_prefix('@')}"
162+
"#{normalized_base}/#{normalized_handle}/"
163+
end
164+
end
165+
end
166+
end
167+
end

0 commit comments

Comments
 (0)