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)
62 params = { status: text }
63 params['media_ids[]'] = media_ids if media_ids
64
65 post_json("/statuses", params)
66 end
67
68 def upload_media(data, filename, content_type, alt = nil)
69 url = URI("https://#{@host}/api/v2/media")
70 headers = { 'Authorization' => "Bearer #{@access_token}" }
71
72 form_data = [
73 ['file', data, { :filename => filename, :content_type => content_type }],
74 ['description', alt.to_s]
75 ]
76
77 request = Net::HTTP::Post.new(url, headers)
78 request.set_form(form_data, 'multipart/form-data')
79
80 response = Net::HTTP.start(url.hostname, url.port, :use_ssl => true) do |http|
81 http.request(request)
82 end
83
84 if response.code.to_i / 100 == 2
85 JSON.parse(response.body)
86 else
87 raise APIError.new(response)
88 end
89 end
90
91 def get_json(path, params = {})
92 url = URI(path.start_with?('https://') ? path : @root + path)
93 url.query = URI.encode_www_form(params) if params
94
95 headers = {}
96 headers['Authorization'] = "Bearer #{@access_token}" if @access_token
97
98 response = Net::HTTP.get_response(url, headers)
99 status = response.code.to_i
100
101 if status / 100 == 2
102 JSON.parse(response.body)
103 elsif status / 100 == 3
104 get_json(response['Location'])
105 else
106 raise APIError.new(response)
107 end
108 end
109
110 def post_json(path, params = {})
111 url = URI(path.start_with?('https://') ? path : @root + path)
112
113 headers = {}
114 headers['Authorization'] = "Bearer #{@access_token}" if @access_token
115
116 request = Net::HTTP::Post.new(url, headers)
117 request.form_data = params
118
119 response = Net::HTTP.start(url.hostname, url.port, :use_ssl => true) do |http|
120 http.request(request)
121 end
122
123 status = response.code.to_i
124
125 if status / 100 == 2
126 JSON.parse(response.body)
127 else
128 raise APIError.new(response)
129 end
130 end
131end