Toot toooooooot (Bluesky-Mastodon cross-poster)
1require 'json'
2require 'net/http'
3require 'uri'
4
5class MastodonAPI
6 class UnauthenticatedError < StandardError
7 end
8
9 class UnexpectedResponseError < StandardError
10 end
11
12 class APIError < StandardError
13 attr_reader :response
14
15 def initialize(response)
16 @response = response
17 super("APIError #{response.code}: #{response.body}")
18 end
19
20 def status
21 response.code.to_i
22 end
23 end
24
25 attr_accessor :access_token
26
27 def initialize(host, access_token = nil)
28 @host = host
29 @root = "https://#{@host}/api/v1"
30 @access_token = access_token
31 end
32
33 def oauth_login_with_password(client_id, client_secret, email, password, scopes)
34 params = {
35 client_id: client_id,
36 client_secret: client_secret,
37 grant_type: 'password',
38 scope: scopes,
39 username: email,
40 password: password
41 }
42
43 post_json("https://#{@host}/oauth/token", params)
44 end
45
46 def account_info
47 raise UnauthenticatedError.new unless @access_token
48 get_json("/accounts/verify_credentials")
49 end
50
51 def lookup_account(username)
52 json = get_json("/accounts/lookup", { acct: username })
53 raise UnexpectedResponseError.new unless json.is_a?(Hash) && json['id'].is_a?(String)
54 json
55 end
56
57 def account_statuses(user_id, params = {})
58 get_json("/accounts/#{user_id}/statuses", params)
59 end
60
61 def post_status(text, media_ids = nil, parent_id = nil)
62 params = { status: text }
63 params['media_ids[]'] = media_ids if media_ids
64 params['in_reply_to_id'] = parent_id if parent_id
65
66 post_json("/statuses", params)
67 end
68
69 def upload_media(data, filename, content_type, alt = nil)
70 url = URI("https://#{@host}/api/v2/media")
71 headers = { 'Authorization' => "Bearer #{@access_token}" }
72
73 form_data = [
74 ['file', data, { :filename => filename, :content_type => content_type }],
75 ['description', alt.to_s.force_encoding('ASCII-8BIT')]
76 ]
77
78 request = Net::HTTP::Post.new(url, headers)
79 request.set_form(form_data, 'multipart/form-data')
80
81 response = Net::HTTP.start(url.hostname, url.port, :use_ssl => true) do |http|
82 http.request(request)
83 end
84
85 if response.code.to_i / 100 == 2
86 JSON.parse(response.body)
87 else
88 raise APIError.new(response)
89 end
90 end
91
92 def get_json(path, params = {})
93 url = URI(path.start_with?('https://') ? path : @root + path)
94 url.query = URI.encode_www_form(params) if params
95
96 headers = {}
97 headers['Authorization'] = "Bearer #{@access_token}" if @access_token
98
99 response = Net::HTTP.get_response(url, headers)
100 status = response.code.to_i
101
102 if status / 100 == 2
103 JSON.parse(response.body)
104 elsif status / 100 == 3
105 get_json(response['Location'])
106 else
107 raise APIError.new(response)
108 end
109 end
110
111 def post_json(path, params = {})
112 url = URI(path.start_with?('https://') ? path : @root + path)
113
114 headers = {}
115 headers['Authorization'] = "Bearer #{@access_token}" if @access_token
116
117 request = Net::HTTP::Post.new(url, headers)
118 request.form_data = params
119
120 response = Net::HTTP.start(url.hostname, url.port, :use_ssl => true) do |http|
121 http.request(request)
122 end
123
124 status = response.code.to_i
125
126 if status / 100 == 2
127 JSON.parse(response.body)
128 else
129 raise APIError.new(response)
130 end
131 end
132end