Commit | Line | Data |
---|---|---|
1882b5bc | 1 | #!/usr/bin/env python3 |
149bc23e AS |
2 | # Copyright (C) 2019 Alex Schroeder <alex@gnu.org> |
3 | ||
4 | # This program is free software: you can redistribute it and/or modify it under | |
5 | # the terms of the GNU Affero General Public License as published by the Free | |
6 | # Software Foundation, either version 3 of the License, or (at your option) any | |
7 | # later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, but WITHOUT | |
10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
11 | # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more | |
12 | # details. | |
13 | # | |
14 | # You should have received a copy of the GNU Affero General Public License along | |
15 | # with this program. If not, see <https://www.gnu.org/licenses/>. | |
16 | ||
17 | import diaspy | |
6328099f | 18 | import subprocess |
149bc23e | 19 | import argparse |
6328099f | 20 | import shutil |
149bc23e AS |
21 | import cmd |
22 | import sys | |
23 | import os | |
24 | ||
25 | # Command abbreviations | |
26 | _ABBREVS = { | |
27 | "q": "quit", | |
6328099f | 28 | "p": "print", |
1badc74e | 29 | "c": "comments", |
bb8316e5 AS |
30 | "r": "reload", |
31 | "n": "notifications", | |
149bc23e AS |
32 | } |
33 | ||
1882b5bc AS |
34 | _RC_PATHS = ( |
35 | "~/.config/jan-pona-mute/login", | |
36 | "~/.config/.jan-pona-mute", | |
37 | "~/.jan-pona-mute" | |
38 | ) | |
39 | ||
6328099f AS |
40 | _PAGERS = ( |
41 | "mdcat", | |
42 | "fold", | |
43 | "cat" | |
44 | ) | |
45 | ||
1882b5bc | 46 | # Init file finder |
149bc23e | 47 | def get_rcfile(): |
1882b5bc | 48 | for rc_path in _RC_PATHS: |
149bc23e AS |
49 | rcfile = os.path.expanduser(rc_path) |
50 | if os.path.exists(rcfile): | |
51 | return rcfile | |
52 | return None | |
53 | ||
6328099f AS |
54 | # Pager finder |
55 | def get_pager(): | |
56 | for cmd in _PAGERS: | |
57 | pager = shutil.which(cmd) | |
58 | if pager != None: | |
59 | return pager | |
60 | ||
149bc23e AS |
61 | class DiasporaClient(cmd.Cmd): |
62 | ||
63 | prompt = "\x1b[38;5;255m" + "> " + "\x1b[0m" | |
6328099f | 64 | intro = "Welcome to Diaspora! Use the intro command for a quick introduction." |
149bc23e | 65 | |
8bf63019 AS |
66 | header_format = "\x1b[1;38;5;255m" + "%s" + "\x1b[0m" |
67 | ||
149bc23e AS |
68 | username = None |
69 | pod = None | |
70 | password = None | |
6328099f | 71 | pager = None |
149bc23e AS |
72 | |
73 | connection = None | |
18e74209 | 74 | notifications = [] |
1882b5bc | 75 | index = None |
18e74209 | 76 | post = None |
bb8316e5 | 77 | post_cache = {} # key is self.post.uid, and notification.id |
f1bfb7bc | 78 | |
714a2f67 AS |
79 | undo = [] |
80 | ||
149bc23e AS |
81 | |
82 | # dict mapping user ids to usernames | |
83 | users = {} | |
84 | ||
85 | def get_username(self, guid): | |
86 | if guid in self.users: | |
87 | return self.users[guid] | |
88 | else: | |
89 | user = diaspy.people.User(connection = self.connection, guid = guid) | |
90 | self.users[guid] = user.handle() | |
91 | return self.users[guid] | |
92 | ||
6328099f | 93 | def do_intro(self, line): |
d87d6adf | 94 | """Start here.""" |
6328099f | 95 | print(""" |
bb8316e5 AS |
96 | Use the 'account' and 'password' commands to set up your connection, |
97 | then use the 'login' command to log in. If everything works as | |
98 | intended, use the 'save' command to save these commands to an init | |
99 | file. | |
6328099f AS |
100 | |
101 | Once you've listed things such as notifications, enter a number to | |
bb8316e5 | 102 | select the corresponding item. |
6328099f AS |
103 | """) |
104 | ||
149bc23e AS |
105 | def do_account(self, account): |
106 | """Set username and pod using the format username@pod.""" | |
107 | try: | |
108 | (self.username, self.pod) = account.split('@') | |
1882b5bc | 109 | print("Username and pod set: %s@%s" % (self.username, self.pod)) |
149bc23e AS |
110 | except ValueError: |
111 | print("The account must contain an @ character, e.g. kensanata@pluspora.com.") | |
112 | print("Use the account comand to set the account.") | |
113 | ||
114 | def do_info(self, line): | |
115 | """Get some info about things. By default, it is info about yourself.""" | |
116 | print("Info about yourself:") | |
1882b5bc AS |
117 | print("Username: %s" % self.username) |
118 | print("Password: %s" % ("None" if self.password == None else "set")) | |
119 | print("Pod: %s" % self.pod) | |
6328099f | 120 | print("Pager: %s" % self.pager) |
149bc23e AS |
121 | |
122 | def do_password(self, password): | |
123 | """Set the password.""" | |
124 | self.password = (None if self.password == "" else password) | |
1882b5bc AS |
125 | print("Password %s" % ("unset" if self.password == "" else "set")) |
126 | ||
127 | def do_save(self, line): | |
d87d6adf | 128 | """Save your login information to the init file.""" |
1882b5bc | 129 | if self.username == None or self.pod == None: |
bb8316e5 | 130 | print("Use the 'account' command to set username and pod") |
1882b5bc | 131 | elif self.password == None: |
bb8316e5 | 132 | print("Use the 'password' command") |
1882b5bc AS |
133 | else: |
134 | rcfile = get_rcfile() | |
135 | if rcfile == None: | |
136 | rfile = first(_RC_PATHS) | |
137 | seen_account = False | |
138 | seen_password = False | |
139 | seen_login = False | |
140 | changed = False | |
141 | file = [] | |
142 | with open(rcfile, "r") as fp: | |
143 | for line in fp: | |
144 | words = line.strip().split() | |
145 | if words: | |
146 | if words[0] == "account": | |
147 | seen_account = True | |
148 | account = "%s@%s" % (self.username, self.pod) | |
149 | if len(words) > 1 and words[1] != account: | |
150 | line = "account %s\n" % account | |
151 | changed = True | |
152 | elif words[0] == "password": | |
153 | seen_password = True | |
154 | if len(words) > 1 and words[1] != self.password: | |
155 | line = "password %s\n" % self.password | |
156 | changed = True | |
157 | elif words[0] == "login": | |
158 | if seen_account and seen_password: | |
159 | seen_login = True | |
160 | else: | |
161 | # skip login if no account or no password given | |
162 | line = None | |
163 | changed = True | |
164 | if line != None: | |
165 | file.append(line) | |
166 | if not seen_account: | |
167 | file.append("account %s@%s\n" % (self.username, self.pod)) | |
168 | changed = True | |
169 | if not seen_password: | |
170 | file.append("password %s\n" % self.password) | |
171 | changed = True | |
172 | if not seen_login: | |
173 | file.append("login\n") | |
174 | changed = True | |
175 | if changed: | |
176 | if os.path.isfile(rcfile): | |
177 | os.rename(rcfile, rcfile + "~") | |
178 | if not os.path.isdir(os.path.dirname(rcfile)): | |
179 | os.makedirs(os.path.dirname(rcfile)) | |
180 | with open(rcfile, "w") as fp: | |
181 | fp.write("".join(file)) | |
182 | print("Wrote %s" % rcfile) | |
183 | else: | |
184 | print("No changes made, %s left unchanged" % rcfile) | |
149bc23e AS |
185 | |
186 | def do_login(self, line): | |
187 | """Login.""" | |
188 | if line != "": | |
1882b5bc AS |
189 | self.onecmd("account %s" % line) |
190 | if self.username == None or self.pod == None: | |
bb8316e5 | 191 | print("Use the 'account' command to set username and pod") |
1882b5bc | 192 | elif self.password == None: |
bb8316e5 | 193 | print("Use the 'password' command") |
1882b5bc AS |
194 | else: |
195 | self.connection = diaspy.connection.Connection( | |
196 | pod = "https://%s" % self.pod, username = self.username, password = self.password) | |
197 | try: | |
198 | self.connection.login() | |
199 | self.onecmd("notifications") | |
200 | except diaspy.errors.LoginError: | |
201 | print("Login failed") | |
149bc23e | 202 | |
6328099f AS |
203 | def do_pager(self, pager): |
204 | """Set the pager, e.g. to cat""" | |
205 | self.pager = pager | |
206 | print("Pager set: %s" % self.pager) | |
207 | ||
8bf63019 AS |
208 | def header(self, line): |
209 | """Wrap line in header format.""" | |
210 | return self.header_format % line | |
211 | ||
149bc23e | 212 | def do_notifications(self, line): |
bb8316e5 AS |
213 | """List notifications. Use 'notifications reload' to reload them.""" |
214 | if line == "" and self.notifications: | |
215 | print("Redisplaying the notifications in the cache.") | |
216 | print("Use the 'reload' argument to reload them.") | |
217 | elif line == "reload" or not self.notifications: | |
218 | if self.connection == None: | |
219 | print("Use the 'login' command, first.") | |
220 | return | |
221 | self.notifications = diaspy.notifications.Notifications(self.connection).last() | |
222 | if self.notifications: | |
223 | for n, notification in enumerate(self.notifications): | |
224 | print(self.header("%2d. %s %s") % (n+1, notification.when(), notification)) | |
225 | print("Enter a number to select the notification.") | |
226 | else: | |
227 | print("There are no notifications. 😢") | |
149bc23e AS |
228 | |
229 | ### The end! | |
230 | def do_quit(self, *args): | |
231 | """Exit jan-pona-mute.""" | |
232 | print("Be safe!") | |
233 | sys.exit() | |
234 | ||
235 | def default(self, line): | |
236 | if line.strip() == "EOF": | |
237 | return self.onecmd("quit") | |
238 | ||
239 | # Expand abbreviated commands | |
240 | first_word = line.split()[0].strip() | |
241 | if first_word in _ABBREVS: | |
242 | full_cmd = _ABBREVS[first_word] | |
243 | expanded = line.replace(first_word, full_cmd, 1) | |
244 | return self.onecmd(expanded) | |
245 | ||
bb8316e5 AS |
246 | # Finally, see if it's a notification and show it |
247 | self.do_show(line) | |
1882b5bc | 248 | |
bb8316e5 | 249 | def do_show(self, line): |
18e74209 AS |
250 | """Show the post given by the index number. |
251 | The index number must refer to the current list of notifications.""" | |
bb8316e5 AS |
252 | if not self.notifications: |
253 | print("No notifications were loaded.") | |
254 | return | |
255 | if line == "": | |
256 | print("The 'show' command takes a notification number") | |
257 | return | |
1882b5bc | 258 | try: |
bb8316e5 | 259 | n = int(line.strip()) |
18e74209 AS |
260 | notification = self.notifications[n-1] |
261 | self.index = n | |
bb8316e5 AS |
262 | except ValueError: |
263 | print("The 'show' command takes a notification number but '%s' is not a number" % line) | |
264 | return | |
1882b5bc | 265 | except IndexError: |
6328099f | 266 | print("Index too high!") |
1882b5bc AS |
267 | return |
268 | ||
18e74209 | 269 | self.show(notification) |
bb8316e5 | 270 | self.load(notification.about()) |
18e74209 AS |
271 | |
272 | print() | |
273 | self.show(self.post) | |
1882b5bc | 274 | |
bb8316e5 AS |
275 | if(self.post.comments): |
276 | print() | |
88c1a828 AS |
277 | if len(self.post.comments) == 1: |
278 | print("There is 1 comment.") | |
279 | else: | |
280 | print("There are %d comments." % len(self.post.comments)) | |
bb8316e5 AS |
281 | print("Use the 'comments' command to list the latest comments.") |
282 | ||
283 | def load(self, id): | |
284 | """Load the post belonging to the id (from a notification), | |
285 | or get it from the cache.""" | |
286 | if id in self.post_cache: | |
287 | self.post = self.post_cache[id] | |
288 | print("Retrieved post from the cache") | |
289 | else: | |
290 | print("Loading...") | |
291 | self.post = diaspy.models.Post(connection = self.connection, id = id) | |
292 | self.post_cache[id] = self.post | |
293 | return self.post | |
294 | ||
295 | def do_reload(self, line): | |
296 | """Reload the current post.""" | |
297 | if self.post == None: | |
298 | print("Use the 'show' command to show a post, first.") | |
299 | return | |
300 | print("Reloading...") | |
301 | self.post = diaspy.models.Post(connection = self.connection, id = self.post.id) | |
302 | self.post_cache[id] = self.post | |
303 | ||
1882b5bc AS |
304 | def show(self, item): |
305 | """Show the current item.""" | |
6328099f | 306 | if self.pager: |
88c1a828 | 307 | subprocess.run(self.pager, input = str(item), text = True) |
6328099f | 308 | else: |
88c1a828 | 309 | print(str(item)) |
6328099f | 310 | |
18e74209 | 311 | def do_comments(self, line): |
bb8316e5 AS |
312 | """Show the comments for the current post. |
313 | Use the 'all' argument to show them all. Use a numerical argument to | |
314 | show that many. The default is to load the last five.""" | |
18e74209 | 315 | if self.post == None: |
bb8316e5 | 316 | print("Use the 'show' command to show a post, first.") |
18e74209 AS |
317 | return |
318 | if self.post.comments == None: | |
319 | print("The current post has no comments.") | |
320 | return | |
321 | ||
322 | n = 5 | |
323 | comments = self.post.comments | |
324 | ||
325 | if line == "all": | |
326 | n = None | |
327 | elif line != "": | |
6328099f AS |
328 | try: |
329 | n = int(line.strip()) | |
330 | except ValueError: | |
bb8316e5 | 331 | print("The 'comments' command takes a number as its argument, or 'all'") |
18e74209 | 332 | print("The default is to show the last 5 comments") |
6328099f | 333 | return |
6328099f | 334 | |
18e74209 AS |
335 | if n != None: |
336 | comments = comments[-n:] | |
337 | ||
42716c49 AS |
338 | if comments: |
339 | for n, comment in enumerate(comments): | |
340 | print() | |
341 | print(self.header("%2d. %s %s") % (n+1, comment.when(), comment.author())) | |
342 | print() | |
343 | self.show(comment) | |
344 | else: | |
345 | print("There are no comments on the selected post.") | |
1882b5bc | 346 | |
714a2f67 AS |
347 | def do_comment(self, line): |
348 | """Leave a comment on the current post.""" | |
349 | if self.post == None: | |
bb8316e5 | 350 | print("Use the 'show' command to show a post, first.") |
714a2f67 AS |
351 | return |
352 | comment = self.post.comment(line) | |
353 | self.post.comments.add(comment) | |
bb8316e5 | 354 | self.undo.append("delete comment %s from %s" % (comment.id, self.id)) |
714a2f67 AS |
355 | print("Comment posted.") |
356 | ||
f1bfb7bc AS |
357 | def do_delete(self, line): |
358 | """Delete a comment.""" | |
359 | words = line.strip().split() | |
360 | if words: | |
361 | if words[0] == "comment": | |
362 | if self.post == None: | |
bb8316e5 | 363 | print("Use the 'show' command to show a post, first.") |
f1bfb7bc AS |
364 | return |
365 | if len(words) != 4: | |
bb8316e5 AS |
366 | print("Deleting a comment requires a comment id and a post id.") |
367 | print("delete comment <comment id> from <post id>") | |
f1bfb7bc AS |
368 | return |
369 | self.post_cache[words[3]].delete_comment(words[1]) | |
370 | print("Comment deleted.") | |
371 | else: | |
372 | print("Deleting '%s' is not supported." % words[0]) | |
373 | return | |
374 | else: | |
375 | print("Delete what?") | |
376 | ||
377 | def do_undo(self, line): | |
378 | """Undo an action.""" | |
379 | if line != "": | |
380 | print("Undo does not take an argument.") | |
381 | return | |
382 | if not self.undo: | |
383 | print("There is nothing to undo.") | |
384 | return | |
385 | return self.onecmd(self.undo.pop()) | |
386 | ||
149bc23e AS |
387 | # Main function |
388 | def main(): | |
389 | ||
390 | # Parse args | |
391 | parser = argparse.ArgumentParser(description='A command line Diaspora client.') | |
1882b5bc AS |
392 | parser.add_argument('--no-init-file', dest='init_file', action='store_const', |
393 | const=False, default=True, help='Do not load a init file') | |
149bc23e AS |
394 | args = parser.parse_args() |
395 | ||
396 | # Instantiate client | |
1882b5bc AS |
397 | c = DiasporaClient() |
398 | ||
399 | # Process init file | |
6328099f | 400 | seen_pager = False |
1882b5bc AS |
401 | if args.init_file: |
402 | rcfile = get_rcfile() | |
403 | if rcfile: | |
404 | print("Using init file %s" % rcfile) | |
405 | with open(rcfile, "r") as fp: | |
406 | for line in fp: | |
407 | line = line.strip() | |
6328099f AS |
408 | if line != "": |
409 | c.cmdqueue.append(line) | |
410 | if not seen_pager: | |
411 | seen_pager = line.startswith("pager "); | |
1882b5bc | 412 | else: |
bb8316e5 | 413 | print("Use the 'save' command to save your login sequence to an init file") |
149bc23e | 414 | |
6328099f AS |
415 | if not seen_pager: |
416 | # prepend | |
417 | c.cmdqueue.insert(0, "pager %s" % get_pager()) | |
418 | ||
149bc23e AS |
419 | # Endless interpret loop |
420 | while True: | |
421 | try: | |
422 | c.cmdloop() | |
423 | except KeyboardInterrupt: | |
424 | print("") | |
425 | ||
426 | if __name__ == '__main__': | |
427 | main() |