aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'mastodon')
-rw-r--r--mastodon/Mastodon.py353
-rw-r--r--mastodon/__init__.py2
2 files changed, 355 insertions, 0 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
new file mode 100644
index 0000000..8d54455
--- /dev/null
+++ b/mastodon/Mastodon.py
@@ -0,0 +1,353 @@
1# coding: utf-8
2
3import requests
4import os
5import os.path
6
7class Mastodon:
8 """
9 Super basic but thorough and easy to use mastodon.social
10 api wrapper in python.
11
12 If anything is unclear, check the official API docs at
13 https://github.com/Gargron/mastodon/wiki/API
14
15 Presently, only username-password login is supported, somebody please
16 patch in Real Proper OAuth if desired.
17
18 KNOWN BUGS: Media api does not work, reason unclear.
19 """
20 __DEFAULT_BASE_URL = 'https://mastodon.social'
21
22 ###
23 # Registering apps
24 ###
25 @staticmethod
26 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, to_file = None, api_base_url = __DEFAULT_BASE_URL):
27 """
28 Creates a new app with given client_name and scopes (read, write, follow)
29
30 Specify redirect_uris if you want users to be redirected to a certain page after authenticating.
31 Specify to_file to persist your apps info to a file so you can use them in the constructor.
32 Specify api_base_url if you want to register an app on an instance different from the flagship one.
33
34 Returns client_id and client_secret.
35 """
36 request_data = {
37 'client_name': client_name,
38 'scopes': " ".join(scopes)
39 }
40
41 if redirect_uris != None:
42 request_data['redirect_uris'] = redirect_uris;
43 else:
44 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob';
45
46 response = requests.post(api_base_url + '/api/v1/apps', data = request_data).json()
47
48 if to_file != None:
49 with open(to_file, 'w') as secret_file:
50 secret_file.write(response['client_id'] + '\n')
51 secret_file.write(response['client_secret'] + '\n')
52
53 return (response['client_id'], response['client_secret'])
54
55 ###
56 # Authentication, including constructor
57 ###
58 def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL):
59 """
60 Creates a new API wrapper instance based on the given client_secret and client_id. If you
61 give a client_id and it is not a file, you must also give a secret.
62
63 You can also directly specify an access_token, directly or as a file.
64
65 Specify api_base_url if you wish to talk to an instance other than the flagship one.
66 If a file is given as client_id, read client ID and secret from that file
67 """
68 self.api_base_url = api_base_url
69 self.client_id = client_id
70 self.client_secret = client_secret
71 self.access_token = access_token
72
73 if os.path.isfile(self.client_id):
74 with open(self.client_id, 'r') as secret_file:
75 self.client_id = secret_file.readline().rstrip()
76 self.client_secret = secret_file.readline().rstrip()
77 else:
78 if self.client_secret == None:
79 raise ValueError('Specified client id directly, but did not supply secret')
80
81 if self.access_token != None and os.path.isfile(self.access_token):
82 with open(self.access_token, 'r') as token_file:
83 self.access_token = token_file.readline().rstrip()
84
85 def log_in(self, username, password, scopes = ['read', 'write', 'follow'], to_file = None):
86 """
87 Logs in and sets access_token to what was returned.
88 Can persist access token to file.
89
90 Returns the access_token, as well.
91 """
92 params = self.__generate_params(locals())
93 params['client_id'] = self.client_id
94 params['client_secret'] = self.client_secret
95 params['grant_type'] = 'password'
96 params['scope'] = " ".join(scopes)
97
98 response = self.__api_request('POST', '/oauth/token', params)
99 self.access_token = response['access_token']
100
101 if to_file != None:
102 with open(to_file, 'w') as token_file:
103 token_file.write(response['access_token'] + '\n')
104
105 return response['access_token']
106
107 ###
108 # Reading data: Timelines
109 ##
110 def timeline(self, timeline = 'home', max_id = None, since_id = None, limit = None):
111 """
112 Returns statuses, most recent ones first. Timeline can be home, mentions, public
113 or tag/:hashtag
114 """
115 params = self.__generate_params(locals(), ['timeline'])
116 return self.__api_request('GET', '/api/v1/timelines/' + timeline, params)
117
118 ###
119 # Reading data: Statuses
120 ###
121 def status(self, id):
122 """
123 Returns a status.
124 """
125 return self.__api_request('GET', '/api/v1/statuses/' + str(id))
126
127 def status_context(self, id):
128 """
129 Returns ancestors and descendants of the status.
130 """
131 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context')
132
133 def status_reblogged_by(self, id):
134 """
135 Returns a list of users that have reblogged a status.
136 """
137 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by')
138
139 def status_favourited_by(self, id):
140 """
141 Returns a list of users that have favourited a status.
142 """
143 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by')
144
145 ###
146 # Reading data: Accounts
147 ###
148 def account(self, id):
149 """
150 Returns account.
151 """
152 return self.__api_request('GET', '/api/v1/accounts/' + str(id))
153
154 def account_verify_credentials(self):
155 """
156 Returns authenticated user's account.
157 """
158 return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
159
160 def account_statuses(self, id, max_id = None, since_id = None, limit = None):
161 """
162 Returns statuses by user. Same options as timeline are permitted.
163 """
164 params = self.__generate_params(locals(), ['id'])
165 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses')
166
167 def account_following(self, id):
168 """
169 Returns users the given user is following.
170 """
171 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following')
172
173 def account_followers(self, id):
174 """
175 Returns users the given user is followed by.
176 """
177 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers')
178
179 def account_relationships(self, id):
180 """
181 Returns relationships (following, followed_by, blocking) of the logged in user to
182 a given account. id can be a list.
183 """
184 params = self.__generate_params(locals())
185 return self.__api_request('GET', '/api/v1/accounts/relationships', params)
186
187 def account_suggestions(self):
188 """
189 Returns accounts that the system suggests the authenticated user to follow.
190 """
191 return self.__api_request('GET', '/api/v1/accounts/suggestions')
192
193 def account_search(self, q, limit = None):
194 """
195 Returns matching accounts. Will lookup an account remotely if the search term is
196 in the username@domain format and not yet in the database.
197 """
198 params = self.__generate_params(locals())
199 return self.__api_request('GET', '/api/v1/accounts/search', params)
200
201 ###
202 # Writing data: Statuses
203 ###
204 def status_post(self, status, in_reply_to_id = None, media_ids = None):
205 """
206 Posts a status. Can optionally be in reply to another status and contain
207 up to four pieces of media (Uploaded via media_post()).
208
209 Returns the new status.
210 """
211 params = self.__generate_params(locals())
212 return self.__api_request('POST', '/api/v1/statuses', params)
213
214 def toot(self, status):
215 """
216 Synonym for status_post that only takes the status text as input.
217 """
218 return self.status_post(status)
219
220 def status_delete(self, id):
221 """
222 Deletes a status
223 """
224 return self.__api_request('DELETE', '/api/v1/statuses/' + str(id))
225
226 def status_reblog(self, id):
227 """Reblogs a status.
228
229 Returns a new status that wraps around the reblogged one."""
230 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog")
231
232 def status_unreblog(self, id):
233 """
234 Un-reblogs a status.
235
236 Returns the status that used to be reblogged.
237 """
238 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog")
239
240 def status_favourite(self, id):
241 """
242 Favourites a status.
243
244 Returns the favourited status.
245 """
246 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite")
247
248 def status_unfavourite(self, id):
249 """Favourites a status.
250
251 Returns the un-favourited status.
252 """
253 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite")
254
255 ###
256 # Writing data: Statuses
257 ###
258 def account_follow(self, id):
259 """
260 Follows a user.
261
262 Returns the updated relationship to the user.
263 """
264 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow")
265
266 def account_unfollow(self, id):
267 """
268 Unfollows a user.
269
270 Returns the updated relationship to the user.
271 """
272 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow")
273
274 def account_block(self, id):
275 """
276 Blocks a user.
277
278 Returns the updated relationship to the user.
279 """
280 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block")
281
282 def account_unblock(self, id):
283 """
284 Unblocks a user.
285
286 Returns the updated relationship to the user.
287 """
288 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock")
289
290 ###
291 # Writing data: Media
292 ###
293 def media_post(self, media_file):
294 """
295 Posts an image. media_file can either be image data or
296 a file name.
297
298 Returns the ID of the media that can then be used in status_post().
299 """
300 if os.path.isfile(media_file):
301 media_file = open(media_file, 'rb')
302
303 return self.__api_request('POST', '/api/v1/media', files = {'file': media_file})
304
305 ###
306 # Internal helpers, dragons probably
307 ###
308 def __api_request(self, method, endpoint, params = {}, files = {}):
309 """
310 Internal API request helper.
311 """
312 response = None
313 headers = None
314
315 if self.access_token != None:
316 headers = {'Authorization': 'Bearer ' + self.access_token}
317
318 if method == 'GET':
319 response = requests.get(self.api_base_url + endpoint, data = params, headers = headers, files = files)
320
321 if method == 'POST':
322 response = requests.post(self.api_base_url + endpoint, data = params, headers = headers, files = files)
323
324 if method == 'DELETE':
325 response = requests.delete(self.api_base_url + endpoint, data = params, headers = headers, files = files)
326
327 if response.status_code == 404:
328 raise IOError('Endpoint not found.')
329
330 if response.status_code == 500:
331 raise IOError('General API problem.')
332
333 return response.json()
334
335 def __generate_params(self, params, exclude = []):
336 """
337 Internal named-parameters-to-dict helper.
338 """
339 params = dict(params)
340
341 del params['self']
342 param_keys = list(params.keys())
343 for key in param_keys:
344 if params[key] == None or key in exclude:
345 del params[key]
346
347 param_keys = list(params.keys())
348 for key in param_keys:
349 if isinstance(params[key], list):
350 params[key + "[]"] = params[key]
351 del params[key]
352
353 return params
diff --git a/mastodon/__init__.py b/mastodon/__init__.py
new file mode 100644
index 0000000..0c0744e
--- /dev/null
+++ b/mastodon/__init__.py
@@ -0,0 +1,2 @@
1from Mastodon import Mastodon
2__all__ = ['Mastodon']
Powered by cgit v1.2.3 (git 2.41.0)