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