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