Toot toooooooot (Bluesky-Mastodon cross-poster)
at master 4.5 kB view raw
1require 'json' 2require 'net/http' 3require 'uri' 4 5class MastodonAPI 6 CODE_REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' 7 8 class UnauthenticatedError < StandardError 9 end 10 11 class UnexpectedResponseError < StandardError 12 end 13 14 class APIError < StandardError 15 attr_reader :response 16 17 def initialize(response) 18 @response = response 19 super("APIError #{response.code}: #{response.body}") 20 end 21 22 def status 23 response.code.to_i 24 end 25 end 26 27 MEDIA_CHECK_INTERVAL = 5.0 28 29 attr_accessor :access_token 30 31 def initialize(host, access_token = nil) 32 @host = host 33 @root = "https://#{@host}/api/v1" 34 @access_token = access_token 35 end 36 37 def register_oauth_app(app_name, scopes) 38 params = { 39 client_name: app_name, 40 redirect_uris: CODE_REDIRECT_URI, 41 scopes: scopes 42 } 43 44 post_json("https://#{@host}/api/v1/apps", params) 45 end 46 47 def generate_oauth_login_url(client_id, scopes) 48 login_url = URI("https://#{@host}/oauth/authorize") 49 50 login_url.query = URI.encode_www_form( 51 client_id: client_id, 52 redirect_uri: CODE_REDIRECT_URI, 53 response_type: 'code', 54 scope: scopes 55 ) 56 57 login_url 58 end 59 60 def complete_oauth_login(client_id, client_secret, code) 61 params = { 62 client_id: client_id, 63 client_secret: client_secret, 64 redirect_uri: CODE_REDIRECT_URI, 65 grant_type: 'authorization_code', 66 code: code 67 } 68 69 post_json("https://#{@host}/oauth/token", params) 70 end 71 72 def account_info 73 raise UnauthenticatedError.new unless @access_token 74 get_json("/accounts/verify_credentials") 75 end 76 77 def instance_info 78 get_json("https://#{@host}/api/v2/instance") 79 end 80 81 def lookup_account(username) 82 json = get_json("/accounts/lookup", { acct: username }) 83 raise UnexpectedResponseError.new unless json.is_a?(Hash) && json['id'].is_a?(String) 84 json 85 end 86 87 def account_statuses(user_id, params = {}) 88 get_json("/accounts/#{user_id}/statuses", params) 89 end 90 91 def post_status(text, media_ids: nil, parent_id: nil, quoted_status_id: nil) 92 params = { status: text } 93 params['media_ids[]'] = media_ids if media_ids 94 params['in_reply_to_id'] = parent_id if parent_id 95 params['quoted_status_id'] = quoted_status_id if quoted_status_id 96 97 post_json("/statuses", params) 98 end 99 100 def upload_media(data, filename, content_type, alt = nil) 101 url = URI("https://#{@host}/api/v2/media") 102 headers = { 'Authorization' => "Bearer #{@access_token}" } 103 104 form_data = [ 105 ['file', data, { :filename => filename, :content_type => content_type }] 106 ] 107 108 if alt 109 form_data << ['description', alt.force_encoding('ASCII-8BIT')] 110 end 111 112 request = Net::HTTP::Post.new(url, headers) 113 request.set_form(form_data, 'multipart/form-data') 114 115 response = Net::HTTP.start(url.hostname, url.port, :use_ssl => true) do |http| 116 http.request(request) 117 end 118 119 json = if response.code.to_i / 100 == 2 120 JSON.parse(response.body) 121 else 122 raise APIError.new(response) 123 end 124 125 while json['url'].nil? 126 sleep MEDIA_CHECK_INTERVAL 127 json = get_media(json['id']) 128 end 129 130 json 131 end 132 133 def get_media(media_id) 134 get_json("/media/#{media_id}") 135 end 136 137 def search_post_by_url(url) 138 json = get_json("https://#{@host}/api/v2/search", { 139 q: url, 140 type: 'statuses', 141 resolve: true 142 }) 143 144 json['statuses'] && json['statuses'][0] 145 end 146 147 def get_json(path, params = {}) 148 url = URI(path.start_with?('https://') ? path : @root + path) 149 url.query = URI.encode_www_form(params) if params 150 151 headers = {} 152 headers['Authorization'] = "Bearer #{@access_token}" if @access_token 153 154 response = Net::HTTP.get_response(url, headers) 155 status = response.code.to_i 156 157 if status / 100 == 2 158 JSON.parse(response.body) 159 elsif status / 100 == 3 160 get_json(response['Location']) 161 else 162 raise APIError.new(response) 163 end 164 end 165 166 def post_json(path, params = {}) 167 url = URI(path.start_with?('https://') ? path : @root + path) 168 169 headers = {} 170 headers['Authorization'] = "Bearer #{@access_token}" if @access_token 171 172 request = Net::HTTP::Post.new(url, headers) 173 request.form_data = params 174 175 response = Net::HTTP.start(url.hostname, url.port, :use_ssl => true) do |http| 176 http.request(request) 177 end 178 179 status = response.code.to_i 180 181 if status / 100 == 2 182 JSON.parse(response.body) 183 else 184 raise APIError.new(response) 185 end 186 end 187end