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