├── LICENSE.md ├── README.md └── libodon.js /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jonas 'Zatnosk' 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libodonjs 2 | JavaScript library for interfacing with mastodon 3 | 4 | https://github.com/Gargron/mastodon/wiki/API 5 | 6 | # Notice 7 | 8 | This library isn't finished, and doesn't yet support posting to a mastodon instance. 9 | 10 | # Registering an app 11 | ``` 12 | var app = new Libodon(app_name, redirect_url) 13 | var connection = app.connect(server_url, user_email) 14 | connection.then(conn=>{ 15 | if(conn.result == 'redirect') { 16 | // conn.target now holds an URL that the browser / user agent should be directed to 17 | } else if(conn.result == 'success'){ 18 | // connection is successful and the app is usable 19 | } 20 | }) 21 | ``` 22 | `app_name` is the name of your app - can be anything. 23 | `redirect_url` should be the address of your app, but can be the special value of `urn:ietf:wg:oauth:2.0:oob` to ask to server to just provide the auth code. 24 | `server_url` should be the address of your server e.g. `https://mastodon.social` 25 | `user_email` is probably unnecessary, but is intended to be the users login email. 26 | If the auth code is provided without redirecting back to your app, then you need to load your app with the url parameter `code` set to the auth code. E.g. `localhost/app.html?code=XXXXX` 27 | 28 | Once you've loaded your app with the auth code, the connection should succeed, and you're ready to use the rest of the API. 29 | -------------------------------------------------------------------------------- /libodon.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | class Libodon { 3 | constructor(appname, redirect_url, scope){ 4 | this.appname = appname 5 | this.redirect_url = redirect_url 6 | this.scope = scope 7 | } 8 | 9 | connect(server, username){ 10 | let connection_resolve, connection_reject, registration; 11 | connections.push(new Promise((resolve,reject)=>{ 12 | connection_resolve=resolve 13 | connection_reject=reject 14 | })) 15 | active_connection = connections.length-1 16 | return get_registration(server, this) 17 | .then(reg=>{ 18 | registration = reg 19 | return get_token(server, reg) 20 | }) 21 | .then( 22 | token=>{ 23 | connection_resolve({server:server,token:token}) 24 | return {result:'success'} 25 | }, 26 | error=>{ 27 | active_connection = undefined 28 | connection_reject({error:'failed connection'}) 29 | return {result:'redirect',target:get_authorization_url(server, registration, this)} 30 | }) 31 | } 32 | 33 | timeline(target, options){ 34 | let endpoint = '' 35 | switch(target){ 36 | case 'home': endpoint = '/api/v1/timelines/home'; break 37 | case 'mentions': endpoint = '/api/v1/timelines/mentions'; break 38 | case 'public': endpoint = '/api/v1/timelines/public'; break 39 | default: 40 | if(target.substring(0,1)=='#') 41 | endpoint = '/api/v1/timelines/tag/'+target.substring(1) 42 | break 43 | } 44 | if(endpoint=='') return Promise.reject('invalid timeline target') 45 | return get_request(endpoint+timeline_options(options)) 46 | } 47 | 48 | status(id){return get_request('/api/v1/statuses/'+id)} 49 | account(id){return get_request('/api/v1/accounts/'+id)} 50 | account_self(){return get_request('/api/v1/accounts/verify_credentials')} 51 | account_statuses(id,options){ 52 | return get_request('/api/v1/accounts/'+id+'/statuses'+timeline_options(options)) 53 | } 54 | followers(id){return get_request('/api/v1/accounts/'+id+'/followers')} 55 | relationships(...ids){ 56 | if(!ids.length) return Promise.reject('no id given') 57 | let query_parameters = '?' 58 | if(ids.length == 1) query_parameters += 'id='+ids[0] 59 | else query_parameters += 'id[]='+ids.join('&id[]=') 60 | return get_request('/api/v1/accounts/relationships'+query_parameters) 61 | } 62 | suggestions(){return get_request('/api/v1/accounts/suggestions')} 63 | context(id){return get_request('/api/v1/statuses/'+id+'/context')} 64 | reblogged_by(id){return get_request('/api/v1/statuses/'+id+'/reblogged_by')} 65 | favourited_by(id){return get_request('/api/v1/statuses/'+id+'/favourited_by')} 66 | 67 | follow_remote(url){return post_request('/api/v1/follows',{uri:url})} 68 | reblog(id){return post_request('/api/v1/statuses/'+id+'/reblog')} 69 | unreblog(id){return post_request('/api/v1/statuses/'+id+'/unreblog')} 70 | favourite(id){return post_request('/api/v1/statuses/'+id+'/favourite')} 71 | unfavourite(id){return post_request('/api/v1/statuses/'+id+'/unfavourite')} 72 | follow(id){return post_request('/api/v1/accounts/'+id+'/follow')} 73 | unfollow(id){return post_request('/api/v1/accounts/'+id+'/unfollow')} 74 | block(id){return post_request('/api/v1/accounts/'+id+'/block')} 75 | unblock(id){return post_request('/api/v1/accounts/'+id+'/unblock')} 76 | 77 | use_errorlog(){log_errors=true} 78 | use_actionlog(){log_actions=true} 79 | } 80 | this.Libodon = Libodon 81 | 82 | const connections = [] 83 | let active_connection = undefined; 84 | 85 | const prefix = 'libodon' 86 | let log_errors = false 87 | let log_actions = false 88 | 89 | function timeline_options(options){ 90 | if(typeof options == 'object'){ 91 | const params = [] 92 | if(options.max_id) params.push('max_id='+options.max_id) 93 | if(options.since_id) params.push('since_id='+options.since_id) 94 | if(options.limit) params.push('limit='+options.limit) 95 | if(options.only_media) params.push('only_media=1') 96 | if(options.local) params.push('local=1') 97 | if(params.length) return '?'+params.join('&') 98 | } 99 | return '' 100 | } 101 | 102 | function get_request(endpoint){ 103 | if(connections.length == 0 104 | || typeof active_connection=='undefined' 105 | || typeof connections[active_connection]=='undefined'){ 106 | return Promise.reject('not connected') 107 | } 108 | return connections[active_connection].then(conn=>{ 109 | if(conn.error) return Promise.reject('not connected') 110 | const server = conn.server; 111 | const token = conn.token.access_token; 112 | const fetchHeaders = new Headers(); 113 | fetchHeaders.set('Authorization','Bearer '+token); 114 | const fetchInit = { 115 | method:'GET', 116 | mode:'cors', 117 | headers: fetchHeaders 118 | } 119 | return fetch(server+endpoint, fetchInit).then(res=>res.json()); 120 | }) 121 | } 122 | 123 | function post_request(endpoint,data){ 124 | if(connections.length == 0 125 | || typeof active_connection=='undefined' 126 | || typeof connections[active_connection]=='undefined'){ 127 | return Promise.reject('not connected') 128 | } 129 | return connections[active_connection].then(conn=>{ 130 | if(conn.error) return Promise.reject('not connected') 131 | const server = conn.server; 132 | const token = conn.token.access_token; 133 | const fetchHeaders = new Headers(); 134 | fetchHeaders.set('Authorization','Bearer '+token); 135 | const body = new URLSearchParams() 136 | for(var key in data) body.set(key,data[key]) 137 | const fetchInit = { 138 | method:'POST', 139 | mode:'cors', 140 | headers: fetchHeaders, 141 | body: body 142 | } 143 | return fetch(server+endpoint, fetchInit).then(res=>res.json()); 144 | }) 145 | } 146 | 147 | function get_token(server, registration){ 148 | const token = localStorage.getItem(prefix+'_token_'+server) 149 | if(typeof token != 'string'){ 150 | const re_match = /[?&]code=([^&]+)/.exec(window.location.search) 151 | if(!re_match){ 152 | if(log_errors) console.error("Failed to find token in storage & no code found in URL parameters.") 153 | throw('no_token_or_code') 154 | } 155 | if(log_actions) console.log('fetching new token') 156 | const code = re_match[1] 157 | const endpoint = server+'/oauth/token' 158 | const data = new URLSearchParams() 159 | data.set('grant_type','authorization_code') 160 | data.set('client_id',registration.client_id) 161 | data.set('client_secret',registration.client_secret) 162 | data.set('redirect_uri',registration.redirect_uri) 163 | data.set('code',code) 164 | const fetchInit = { 165 | method:'POST', 166 | mode:'cors', 167 | body: data 168 | } 169 | return fetch(endpoint,fetchInit).then(res=>res.json()).then(obj=>{ 170 | if(obj.error=='invalid_grant'){ 171 | if(log_errors) console.error(obj.error_description) 172 | throw obj.error 173 | } 174 | localStorage.setItem(prefix+'_token_'+server,JSON.stringify(obj)) 175 | return obj 176 | }) 177 | } else { 178 | if(log_actions) console.log('reading token from storage') 179 | return new Promise(resolve=>resolve(JSON.parse(token))) 180 | } 181 | 182 | } 183 | 184 | function get_authorization_url(server, registration, libodon){ 185 | let endpoint = server+'/oauth/authorize?response_type=code' 186 | endpoint += '&client_id='+registration.client_id 187 | endpoint += '&redirect_uri='+registration.redirect_uri 188 | if(libodon.scope) endpoint += '&scope='+encodeURI(libodon.scope) 189 | return endpoint 190 | } 191 | 192 | function get_registration(server, libodon){ 193 | const reg = localStorage.getItem(prefix+'_registration_'+server) 194 | if(typeof reg != 'string'){ 195 | if(log_actions) console.log('registering new app') 196 | const promise = register_application(server, libodon) 197 | return promise.then(reg=>{ 198 | localStorage.setItem(prefix+'_registration_'+server,JSON.stringify(reg)) 199 | return reg 200 | }) 201 | } else { 202 | if(log_actions) console.log('reading registration from storage') 203 | return new Promise(resolve=>resolve(JSON.parse(reg))) 204 | } 205 | } 206 | 207 | function register_application(server, libodon){ 208 | const endpoint = server+'/api/v1/apps' 209 | const data = new URLSearchParams() 210 | data.set('response_type','code') 211 | data.set('client_name',libodon.appname) 212 | data.set('redirect_uris',libodon.redirect_url) 213 | data.set('scopes',libodon.scope) 214 | const fetchInit = { 215 | method:'POST', 216 | mode:'cors', 217 | body: data 218 | } 219 | return fetch(endpoint,fetchInit).then(res=>res.json()) 220 | } 221 | })() 222 | --------------------------------------------------------------------------------