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 | ||
1882b5bc AS |
25 | _RC_PATHS = ( |
26 | "~/.config/jan-pona-mute/login", | |
ebc699fd | 27 | "~/.jan-pona-mute.d/login" |
1882b5bc AS |
28 | "~/.jan-pona-mute" |
29 | ) | |
30 | ||
ebc699fd AS |
31 | _NOTE_DIRS = ( |
32 | "~/.config/jan-pona-mute/notes", | |
33 | "~/.jan-pona-mute.d/notes" | |
34 | ) | |
35 | ||
6328099f | 36 | _PAGERS = ( |
ebc699fd | 37 | os.getenv("PAGER"), |
6328099f AS |
38 | "mdcat", |
39 | "fold", | |
40 | "cat" | |
41 | ) | |
42 | ||
ebc699fd AS |
43 | _EDITORS = ( |
44 | os.getenv("EDITOR"), | |
45 | "vi", | |
46 | "ed" | |
47 | ) | |
48 | ||
82f0d747 AS |
49 | shortcuts = { |
50 | "q": "quit", | |
51 | "p": "preview", | |
52 | "c": "comments", | |
53 | "r": "reload", | |
54 | "n": "notifications", | |
55 | "e": "edit", | |
56 | "d": "delete", | |
57 | "g": "notifications reload", | |
58 | } | |
59 | ||
149bc23e | 60 | def get_rcfile(): |
ebc699fd AS |
61 | """Init file finder""" |
62 | for path in _RC_PATHS: | |
63 | path = os.path.expanduser(path) | |
64 | if os.path.exists(path): | |
65 | return path | |
149bc23e AS |
66 | return None |
67 | ||
ebc699fd AS |
68 | def get_notes_dir(): |
69 | """Notes directory finder""" | |
70 | dir = None | |
71 | for path in _NOTE_DIRS: | |
72 | path = os.path.expanduser(path) | |
73 | if os.path.isdir(path): | |
74 | dir = path | |
75 | break | |
76 | if dir == None: | |
77 | dir = os.path.expanduser(_NOTE_DIRS[0]) | |
78 | if not os.path.isdir(dir): | |
79 | os.makedirs(dir) | |
80 | return dir | |
81 | ||
82 | def get_binary(list): | |
83 | for cmd in list: | |
84 | if cmd != None: | |
85 | bin = shutil.which(cmd) | |
86 | if bin != None: | |
87 | return bin | |
88 | ||
6328099f | 89 | def get_pager(): |
ebc699fd AS |
90 | """Pager finder""" |
91 | return get_binary(_PAGERS) | |
92 | ||
93 | def get_editor(): | |
94 | """Editor finder""" | |
95 | return get_binary(_EDITORS) | |
6328099f | 96 | |
149bc23e AS |
97 | class DiasporaClient(cmd.Cmd): |
98 | ||
99 | prompt = "\x1b[38;5;255m" + "> " + "\x1b[0m" | |
6328099f | 100 | intro = "Welcome to Diaspora! Use the intro command for a quick introduction." |
149bc23e | 101 | |
8bf63019 AS |
102 | header_format = "\x1b[1;38;5;255m" + "%s" + "\x1b[0m" |
103 | ||
149bc23e AS |
104 | username = None |
105 | pod = None | |
106 | password = None | |
6328099f | 107 | pager = None |
ebc699fd | 108 | editor = None |
149bc23e AS |
109 | |
110 | connection = None | |
18e74209 | 111 | notifications = [] |
1ff8fa2c | 112 | home = None |
bec46251 | 113 | numbers_refer_to = None |
18e74209 | 114 | post = None |
bb8316e5 | 115 | post_cache = {} # key is self.post.uid, and notification.id |
f1bfb7bc | 116 | |
714a2f67 AS |
117 | undo = [] |
118 | ||
149bc23e AS |
119 | |
120 | # dict mapping user ids to usernames | |
121 | users = {} | |
122 | ||
123 | def get_username(self, guid): | |
124 | if guid in self.users: | |
125 | return self.users[guid] | |
126 | else: | |
127 | user = diaspy.people.User(connection = self.connection, guid = guid) | |
128 | self.users[guid] = user.handle() | |
129 | return self.users[guid] | |
130 | ||
6328099f | 131 | def do_intro(self, line): |
d87d6adf | 132 | """Start here.""" |
6328099f | 133 | print(""" |
bb8316e5 AS |
134 | Use the 'account' and 'password' commands to set up your connection, |
135 | then use the 'login' command to log in. If everything works as | |
136 | intended, use the 'save' command to save these commands to an init | |
137 | file. | |
6328099f | 138 | |
bec46251 AS |
139 | Once you've listed things such as notifications or the home stream, |
140 | enter a number to select the corresponding item. | |
6328099f AS |
141 | """) |
142 | ||
149bc23e AS |
143 | def do_account(self, account): |
144 | """Set username and pod using the format username@pod.""" | |
145 | try: | |
146 | (self.username, self.pod) = account.split('@') | |
1882b5bc | 147 | print("Username and pod set: %s@%s" % (self.username, self.pod)) |
149bc23e AS |
148 | except ValueError: |
149 | print("The account must contain an @ character, e.g. kensanata@pluspora.com.") | |
150 | print("Use the account comand to set the account.") | |
151 | ||
152 | def do_info(self, line): | |
153 | """Get some info about things. By default, it is info about yourself.""" | |
82f0d747 | 154 | print(self.header("Info")) |
1882b5bc AS |
155 | print("Username: %s" % self.username) |
156 | print("Password: %s" % ("None" if self.password == None else "set")) | |
157 | print("Pod: %s" % self.pod) | |
6328099f | 158 | print("Pager: %s" % self.pager) |
ebc699fd | 159 | print("Editor: %s" % self.editor) |
bec46251 | 160 | print("Cache: %s posts" % len(self.post_cache)) |
149bc23e AS |
161 | |
162 | def do_password(self, password): | |
163 | """Set the password.""" | |
164 | self.password = (None if self.password == "" else password) | |
1882b5bc AS |
165 | print("Password %s" % ("unset" if self.password == "" else "set")) |
166 | ||
167 | def do_save(self, line): | |
d87d6adf | 168 | """Save your login information to the init file.""" |
1882b5bc | 169 | if self.username == None or self.pod == None: |
ebc699fd | 170 | print("Use the 'account' command to set username and pod.") |
1882b5bc | 171 | elif self.password == None: |
ebc699fd | 172 | print("Use the 'password' command.") |
1882b5bc AS |
173 | else: |
174 | rcfile = get_rcfile() | |
175 | if rcfile == None: | |
ebc699fd | 176 | rfile = _RC_PATHS[0] |
1882b5bc AS |
177 | seen_account = False |
178 | seen_password = False | |
179 | seen_login = False | |
180 | changed = False | |
181 | file = [] | |
182 | with open(rcfile, "r") as fp: | |
183 | for line in fp: | |
184 | words = line.strip().split() | |
185 | if words: | |
186 | if words[0] == "account": | |
187 | seen_account = True | |
188 | account = "%s@%s" % (self.username, self.pod) | |
189 | if len(words) > 1 and words[1] != account: | |
190 | line = "account %s\n" % account | |
191 | changed = True | |
192 | elif words[0] == "password": | |
193 | seen_password = True | |
194 | if len(words) > 1 and words[1] != self.password: | |
195 | line = "password %s\n" % self.password | |
196 | changed = True | |
197 | elif words[0] == "login": | |
198 | if seen_account and seen_password: | |
199 | seen_login = True | |
200 | else: | |
201 | # skip login if no account or no password given | |
202 | line = None | |
203 | changed = True | |
204 | if line != None: | |
205 | file.append(line) | |
206 | if not seen_account: | |
207 | file.append("account %s@%s\n" % (self.username, self.pod)) | |
208 | changed = True | |
209 | if not seen_password: | |
210 | file.append("password %s\n" % self.password) | |
211 | changed = True | |
212 | if not seen_login: | |
213 | file.append("login\n") | |
214 | changed = True | |
215 | if changed: | |
216 | if os.path.isfile(rcfile): | |
217 | os.rename(rcfile, rcfile + "~") | |
218 | if not os.path.isdir(os.path.dirname(rcfile)): | |
219 | os.makedirs(os.path.dirname(rcfile)) | |
220 | with open(rcfile, "w") as fp: | |
221 | fp.write("".join(file)) | |
222 | print("Wrote %s" % rcfile) | |
223 | else: | |
224 | print("No changes made, %s left unchanged" % rcfile) | |
149bc23e AS |
225 | |
226 | def do_login(self, line): | |
227 | """Login.""" | |
228 | if line != "": | |
1882b5bc AS |
229 | self.onecmd("account %s" % line) |
230 | if self.username == None or self.pod == None: | |
ebc699fd | 231 | print("Use the 'account' command to set username and pod.") |
1882b5bc | 232 | elif self.password == None: |
ebc699fd | 233 | print("Use the 'password' command.") |
1882b5bc AS |
234 | else: |
235 | self.connection = diaspy.connection.Connection( | |
236 | pod = "https://%s" % self.pod, username = self.username, password = self.password) | |
237 | try: | |
238 | self.connection.login() | |
1882b5bc | 239 | except diaspy.errors.LoginError: |
ebc699fd | 240 | print("Login failed.") |
149bc23e | 241 | |
6328099f AS |
242 | def do_pager(self, pager): |
243 | """Set the pager, e.g. to cat""" | |
244 | self.pager = pager | |
245 | print("Pager set: %s" % self.pager) | |
246 | ||
ebc699fd AS |
247 | def do_editor(self, editor): |
248 | """Set the editor, e.g. to ed""" | |
249 | self.editor = editor | |
250 | print("Editor set: %s" % self.editor) | |
251 | ||
8bf63019 AS |
252 | def header(self, line): |
253 | """Wrap line in header format.""" | |
254 | return self.header_format % line | |
255 | ||
149bc23e | 256 | def do_notifications(self, line): |
bb8316e5 AS |
257 | """List notifications. Use 'notifications reload' to reload them.""" |
258 | if line == "" and self.notifications: | |
259 | print("Redisplaying the notifications in the cache.") | |
260 | print("Use the 'reload' argument to reload them.") | |
261 | elif line == "reload" or not self.notifications: | |
262 | if self.connection == None: | |
263 | print("Use the 'login' command, first.") | |
264 | return | |
265 | self.notifications = diaspy.notifications.Notifications(self.connection).last() | |
ebc699fd AS |
266 | else: |
267 | print("The 'notifications' command only takes the optional argument 'reload'.") | |
268 | return | |
bb8316e5 AS |
269 | if self.notifications: |
270 | for n, notification in enumerate(self.notifications): | |
82f0d747 AS |
271 | if notification.unread: |
272 | print(self.header("%2d. %s %s") % (n+1, notification.when(), notification)) | |
273 | else: | |
274 | print("%2d. %s %s" % (n+1, notification.when(), notification)) | |
bb8316e5 | 275 | print("Enter a number to select the notification.") |
bec46251 | 276 | self.numbers_refer_to = 'notifications' |
bb8316e5 AS |
277 | else: |
278 | print("There are no notifications. 😢") | |
149bc23e AS |
279 | |
280 | ### The end! | |
281 | def do_quit(self, *args): | |
282 | """Exit jan-pona-mute.""" | |
283 | print("Be safe!") | |
284 | sys.exit() | |
285 | ||
286 | def default(self, line): | |
287 | if line.strip() == "EOF": | |
288 | return self.onecmd("quit") | |
289 | ||
290 | # Expand abbreviated commands | |
291 | first_word = line.split()[0].strip() | |
82f0d747 AS |
292 | if first_word in shortcuts: |
293 | full_cmd = shortcuts[first_word] | |
149bc23e AS |
294 | expanded = line.replace(first_word, full_cmd, 1) |
295 | return self.onecmd(expanded) | |
296 | ||
bb8316e5 AS |
297 | # Finally, see if it's a notification and show it |
298 | self.do_show(line) | |
1882b5bc | 299 | |
bb8316e5 | 300 | def do_show(self, line): |
18e74209 | 301 | """Show the post given by the index number. |
bec46251 | 302 | The index number must refer to the current list of notifications |
1ff8fa2c AS |
303 | or the home stream. If no index number is given, show the current |
304 | post again.""" | |
bec46251 AS |
305 | if not self.notifications and not self.home: |
306 | print("Use the 'login' command to load notifications.") | |
bb8316e5 | 307 | return |
1ff8fa2c | 308 | if line == "" and self.post == None: |
bec46251 | 309 | print("Please specify a number.") |
bb8316e5 | 310 | return |
1ff8fa2c AS |
311 | if line != "": |
312 | try: | |
313 | n = int(line.strip()) | |
314 | if self.numbers_refer_to == 'notifications': | |
315 | notification = self.notifications[n-1] | |
316 | self.show(notification) | |
317 | self.load(notification.about()) | |
318 | elif self.numbers_refer_to == 'home': | |
319 | self.post = self.home[n-1] | |
320 | else: | |
321 | print("Internal error: not sure what numbers '%s' refer to." % self.numbers_refer_to) | |
322 | return | |
323 | except ValueError: | |
324 | print("The 'show' command takes a notification number but '%s' is not a number" % line) | |
325 | return | |
326 | except IndexError: | |
327 | print("Index too high!") | |
bec46251 | 328 | return |
1882b5bc | 329 | |
18e74209 AS |
330 | print() |
331 | self.show(self.post) | |
1882b5bc | 332 | |
bb8316e5 AS |
333 | if(self.post.comments): |
334 | print() | |
88c1a828 AS |
335 | if len(self.post.comments) == 1: |
336 | print("There is 1 comment.") | |
337 | else: | |
338 | print("There are %d comments." % len(self.post.comments)) | |
bb8316e5 AS |
339 | print("Use the 'comments' command to list the latest comments.") |
340 | ||
341 | def load(self, id): | |
342 | """Load the post belonging to the id (from a notification), | |
343 | or get it from the cache.""" | |
344 | if id in self.post_cache: | |
345 | self.post = self.post_cache[id] | |
ebc699fd | 346 | print("Retrieved post from the cache.") |
bb8316e5 AS |
347 | else: |
348 | print("Loading...") | |
349 | self.post = diaspy.models.Post(connection = self.connection, id = id) | |
350 | self.post_cache[id] = self.post | |
351 | return self.post | |
352 | ||
353 | def do_reload(self, line): | |
354 | """Reload the current post.""" | |
355 | if self.post == None: | |
356 | print("Use the 'show' command to show a post, first.") | |
357 | return | |
358 | print("Reloading...") | |
359 | self.post = diaspy.models.Post(connection = self.connection, id = self.post.id) | |
360 | self.post_cache[id] = self.post | |
361 | ||
1882b5bc AS |
362 | def show(self, item): |
363 | """Show the current item.""" | |
6328099f | 364 | if self.pager: |
88c1a828 | 365 | subprocess.run(self.pager, input = str(item), text = True) |
6328099f | 366 | else: |
88c1a828 | 367 | print(str(item)) |
6328099f | 368 | |
18e74209 | 369 | def do_comments(self, line): |
bb8316e5 AS |
370 | """Show the comments for the current post. |
371 | Use the 'all' argument to show them all. Use a numerical argument to | |
372 | show that many. The default is to load the last five.""" | |
18e74209 | 373 | if self.post == None: |
bb8316e5 | 374 | print("Use the 'show' command to show a post, first.") |
18e74209 AS |
375 | return |
376 | if self.post.comments == None: | |
377 | print("The current post has no comments.") | |
378 | return | |
379 | ||
380 | n = 5 | |
381 | comments = self.post.comments | |
382 | ||
383 | if line == "all": | |
384 | n = None | |
385 | elif line != "": | |
6328099f AS |
386 | try: |
387 | n = int(line.strip()) | |
388 | except ValueError: | |
ebc699fd AS |
389 | print("The 'comments' command takes a number as its argument, or 'all'.") |
390 | print("The default is to show the last 5 comments.") | |
6328099f | 391 | return |
6328099f | 392 | |
ebc699fd AS |
393 | if n == None: |
394 | start = 0 | |
395 | else: | |
396 | # n is from the back | |
397 | start = max(len(comments) - n, 0) | |
18e74209 | 398 | |
42716c49 | 399 | if comments: |
ebc699fd | 400 | for n, comment in enumerate(comments[start:], start): |
42716c49 AS |
401 | print() |
402 | print(self.header("%2d. %s %s") % (n+1, comment.when(), comment.author())) | |
403 | print() | |
404 | self.show(comment) | |
405 | else: | |
406 | print("There are no comments on the selected post.") | |
1882b5bc | 407 | |
714a2f67 | 408 | def do_comment(self, line): |
ebc699fd AS |
409 | """Leave a comment on the current post. |
410 | If you just use a number as your comment, it will refer to a note. | |
411 | Use the 'edit' command to edit notes.""" | |
714a2f67 | 412 | if self.post == None: |
bb8316e5 | 413 | print("Use the 'show' command to show a post, first.") |
714a2f67 | 414 | return |
ebc699fd | 415 | try: |
9200e1e1 | 416 | # if the comment is just a number, use a note to post |
ebc699fd AS |
417 | n = int(line.strip()) |
418 | notes = self.get_notes() | |
419 | if notes: | |
420 | try: | |
7e2b259d | 421 | line = self.read_note(notes[n-1]) |
ebc699fd AS |
422 | print("Using note #%d: %s" % (n, notes[n-1])) |
423 | except IndexError: | |
424 | print("Use the 'list notes' command to list valid numbers.") | |
425 | return | |
426 | else: | |
427 | print("There are no notes to use.") | |
428 | return | |
429 | except ValueError: | |
9200e1e1 AS |
430 | # in which case we'll simply comment with the line |
431 | pass | |
714a2f67 AS |
432 | comment = self.post.comment(line) |
433 | self.post.comments.add(comment) | |
ebc699fd | 434 | self.undo.append("delete comment %s from %s" % (comment.id, self.post.id)) |
714a2f67 AS |
435 | print("Comment posted.") |
436 | ||
1ff8fa2c AS |
437 | def do_post(self, line): |
438 | """Write a post on the current stream. | |
439 | If you just use a number as your post, it will refer to a note. | |
440 | Use the 'edit' command to edit notes.""" | |
441 | if self.home == None: | |
442 | self.home = diaspy.streams.Stream(self.connection) | |
443 | try: | |
444 | # if the post is just a number, use a note to post | |
445 | n = int(line.strip()) | |
446 | notes = self.get_notes() | |
447 | if notes: | |
448 | try: | |
449 | line = self.read_note(notes[n-1]) | |
450 | print("Using note #%d: %s" % (n, notes[n-1])) | |
451 | except IndexError: | |
452 | print("Use the 'list notes' command to list valid numbers.") | |
453 | return | |
454 | else: | |
455 | print("There are no notes to use.") | |
456 | return | |
457 | except ValueError: | |
458 | # in which case we'll simply post the line | |
459 | pass | |
460 | self.post = self.home.post(text = line) | |
461 | self.post_cache[self.post.id] = self.post | |
462 | self.undo.append("delete post %s" % self.post.id) | |
463 | print("Posted. Use the 'show' command to show it.") | |
464 | ||
f1bfb7bc AS |
465 | def do_delete(self, line): |
466 | """Delete a comment.""" | |
467 | words = line.strip().split() | |
468 | if words: | |
469 | if words[0] == "comment": | |
ebc699fd | 470 | if len(words) == 4: |
b5d7f34e AS |
471 | post = self.post_cache[words[3]] |
472 | post.delete_comment(words[1]) | |
473 | comments = [c.id for c in post.comments if c.id != id] | |
474 | post.comments = diaspy.models.Comments(comments) | |
ebc699fd AS |
475 | print("Comment deleted.") |
476 | return | |
b5d7f34e AS |
477 | if self.post == None: |
478 | print("Use the 'show' command to show a post, first.") | |
479 | return | |
ebc699fd AS |
480 | if len(words) == 2: |
481 | try: | |
482 | n = int(words[1]) | |
483 | comment = self.post.comments[n-1] | |
484 | id = comment.id | |
485 | except ValueError: | |
486 | print("Deleting a comment requires an integer.") | |
487 | return | |
488 | except IndexError: | |
489 | print("Use the 'comments' command to find valid comment numbers.") | |
490 | return | |
491 | # not catching any errors from diaspy | |
492 | self.post.delete_comment(id) | |
493 | # there is no self.post.comments.remove(id) | |
494 | comments = [c.id for c in self.post.comments if c.id != id] | |
495 | self.post.comments = diaspy.models.Comments(comments) | |
496 | print("Comment deleted.") | |
497 | return | |
498 | else: | |
499 | print("Deleting a comment requires a comment id and a post id, or a number.") | |
bb8316e5 | 500 | print("delete comment <comment id> from <post id>") |
ebc699fd AS |
501 | print("delete comment 5") |
502 | return | |
503 | if words[0] == "note": | |
504 | if len(words) != 2: | |
505 | print("Deleting a note requires a number.") | |
506 | return | |
507 | try: | |
508 | n = int(words[1]) | |
509 | except ValueError: | |
510 | print("Deleting a note requires an integer.") | |
f1bfb7bc | 511 | return |
ebc699fd AS |
512 | notes = self.get_notes() |
513 | if notes: | |
514 | try: | |
515 | os.unlink(self.get_note_path(notes[n-1])) | |
516 | print("Deleted note #%d: %s" % (n, notes[n-1])) | |
517 | except IndexError: | |
518 | print("Use the 'list notes' command to list valid numbers.") | |
519 | else: | |
520 | print("There are no notes to delete.") | |
f1bfb7bc | 521 | else: |
ebc699fd | 522 | print("Things to delete: comment, note.") |
f1bfb7bc AS |
523 | return |
524 | else: | |
525 | print("Delete what?") | |
526 | ||
527 | def do_undo(self, line): | |
528 | """Undo an action.""" | |
529 | if line != "": | |
ebc699fd | 530 | print("The 'undo' command does not take an argument.") |
f1bfb7bc AS |
531 | return |
532 | if not self.undo: | |
533 | print("There is nothing to undo.") | |
534 | return | |
535 | return self.onecmd(self.undo.pop()) | |
536 | ||
ebc699fd AS |
537 | def do_edit(self, line): |
538 | """Edit a note with a given name.""" | |
539 | if line == "": | |
540 | print("Edit takes the name of a note as an argument.") | |
541 | return | |
542 | file = self.get_note_path(line) | |
543 | if self.editor: | |
544 | subprocess.run([self.editor, file]) | |
cc59946e | 545 | self.onecmd("notes") |
ebc699fd AS |
546 | else: |
547 | print("Use the 'editor' command to set an editor.") | |
548 | ||
549 | def do_notes(self, line): | |
550 | """List notes""" | |
551 | if line != "": | |
552 | print("The 'notes' command does not take an argument.") | |
553 | return | |
554 | notes = self.get_notes() | |
555 | if notes: | |
556 | for n, note in enumerate(notes): | |
557 | print(self.header("%2d. %s") % (n+1, note)) | |
1ff8fa2c AS |
558 | print("Use the 'edit' command to edit a note.") |
559 | print("Use the 'preview' command to look at a note.") | |
560 | print("Use the 'post' command to post a note.") | |
561 | print("Use the 'comment' command to post a comment.") | |
ebc699fd | 562 | else: |
1ff8fa2c | 563 | print("Use 'edit' to create a note.") |
ebc699fd AS |
564 | |
565 | def get_notes(self): | |
566 | """Get the list of notes.""" | |
567 | return os.listdir(get_notes_dir()) | |
568 | ||
569 | def get_note_path(self, filename): | |
570 | """Get the correct path for a note.""" | |
571 | return os.path.join(get_notes_dir(), filename) | |
572 | ||
7e2b259d AS |
573 | def read_note(self, filename): |
574 | """Get text of a note.""" | |
575 | with open(self.get_note_path(filename), mode = 'r', encoding = 'utf-8') as fp: | |
576 | return fp.read() | |
577 | ||
f38b656c AS |
578 | def do_preview(self, line): |
579 | """Preview a note using your pager. | |
580 | Use the 'pager' command to set your pager to something like 'mdcat'.""" | |
581 | if line == "": | |
582 | print("The 'preview' command the number of a note as an argument.") | |
583 | print("Use the 'notes' command to list all your notes.") | |
584 | return | |
585 | try: | |
586 | n = int(line.strip()) | |
587 | notes = self.get_notes() | |
588 | if notes: | |
589 | try: | |
590 | self.show(self.read_note(notes[n-1])) | |
591 | except IndexError: | |
592 | print("Use the 'list notes' command to list valid numbers.") | |
593 | return | |
594 | else: | |
595 | print("There are no notes to preview.") | |
596 | return | |
597 | except ValueError: | |
598 | print("The 'preview' command takes a number as its argument.") | |
599 | return | |
600 | ||
bec46251 AS |
601 | def do_home(self, line): |
602 | """Show the main stream containing the combined posts of the | |
603 | followed users and tags and the community spotlights posts if | |
604 | the user enabled those.""" | |
605 | if line == "": | |
606 | if self.home: | |
607 | print("Redisplaying the cached statuses of the home stream.") | |
608 | print("Use the 'reload' argument to reload them.") | |
609 | print("Use the 'all' argument to show them all.") | |
610 | print("Use a number to show only that many.") | |
611 | print("The default is 5.") | |
612 | else: | |
613 | print("Loading...") | |
614 | self.home = diaspy.streams.Stream(self.connection) | |
615 | self.home.fill() | |
616 | for post in self.home: | |
617 | if post.id not in self.post_cache: | |
618 | self.post_cache[post.id] = post | |
619 | elif line == "reload": | |
620 | if self.connection == None: | |
621 | print("Use the 'login' command, first.") | |
622 | return | |
623 | if self.home: | |
624 | print("Reloading...") | |
625 | self.home.update() | |
626 | line = "" | |
627 | else: | |
628 | self.home = diaspy.streams.Stream(self.connection) | |
629 | self.home.fill() | |
630 | ||
631 | n = 5 | |
1ff8fa2c | 632 | posts = sorted(self.home, key=lambda x: x.data()["created_at"]) |
bec46251 AS |
633 | |
634 | if line == "all": | |
635 | n = None | |
636 | elif line != "": | |
637 | try: | |
638 | n = int(line.strip()) | |
639 | except ValueError: | |
640 | print("The 'home' command takes a number as its argument, or 'reload' or 'all'.") | |
641 | print("The default is to show the last 5 posts.") | |
642 | return | |
643 | ||
644 | if n == None: | |
645 | start = 0 | |
646 | else: | |
647 | # n is from the back | |
648 | start = max(len(posts) - n, 0) | |
649 | ||
650 | if posts: | |
651 | for n, post in enumerate(posts[start:], start): | |
652 | print() | |
653 | print(self.header("%2d. %s %s") % (n+1, post.data()["created_at"], post.author())) | |
654 | print() | |
655 | self.show(post) | |
656 | ||
657 | print("Enter a number to select the post.") | |
658 | self.numbers_refer_to = 'home' | |
659 | else: | |
660 | print("The people you follow have nothing to say.") | |
661 | print("The tags you follow are empty. 😢") | |
662 | ||
82f0d747 AS |
663 | def do_shortcuts(self, line): |
664 | """List all shortcuts.""" | |
665 | if line != "": | |
666 | print("The 'shortcuts' command does not take an argument.") | |
667 | return | |
668 | print(self.header("Shortcuts")) | |
669 | for shortcut in sorted(shortcuts): | |
670 | print("%s\t%s" % (shortcut, shortcuts[shortcut])) | |
671 | print("Use the 'shortcut' command to change or add shortcuts.") | |
672 | ||
673 | def do_shortcut(self, line): | |
674 | """Define a new shortcut.""" | |
675 | words = line.strip().split(maxsplit = 1) | |
676 | if len(words) == 0: | |
677 | return self.onecmd("shortcuts") | |
678 | elif len(words) == 1: | |
679 | shortcut = words[0] | |
680 | if shortcut in shortcuts: | |
681 | print("%s\t%s" % (shortcut, shortcuts[shortcut])) | |
682 | else: | |
683 | print("%s is not a shortcut" % shortcut) | |
684 | else: | |
685 | shortcuts[words[0]] = words[1] | |
686 | ||
149bc23e AS |
687 | # Main function |
688 | def main(): | |
689 | ||
690 | # Parse args | |
691 | parser = argparse.ArgumentParser(description='A command line Diaspora client.') | |
1882b5bc AS |
692 | parser.add_argument('--no-init-file', dest='init_file', action='store_const', |
693 | const=False, default=True, help='Do not load a init file') | |
149bc23e AS |
694 | args = parser.parse_args() |
695 | ||
696 | # Instantiate client | |
1882b5bc AS |
697 | c = DiasporaClient() |
698 | ||
699 | # Process init file | |
6328099f | 700 | seen_pager = False |
ebc699fd | 701 | seen_editor = False |
1882b5bc AS |
702 | if args.init_file: |
703 | rcfile = get_rcfile() | |
704 | if rcfile: | |
705 | print("Using init file %s" % rcfile) | |
706 | with open(rcfile, "r") as fp: | |
707 | for line in fp: | |
708 | line = line.strip() | |
6328099f AS |
709 | if line != "": |
710 | c.cmdqueue.append(line) | |
711 | if not seen_pager: | |
712 | seen_pager = line.startswith("pager "); | |
ebc699fd AS |
713 | if not seen_editor: |
714 | seen_editor = line.startswith("editor "); | |
1882b5bc | 715 | else: |
ebc699fd | 716 | print("Use the 'save' command to save your login sequence to an init file.") |
149bc23e | 717 | |
6328099f AS |
718 | if not seen_pager: |
719 | # prepend | |
720 | c.cmdqueue.insert(0, "pager %s" % get_pager()) | |
ebc699fd AS |
721 | if not seen_editor: |
722 | # prepend | |
723 | c.cmdqueue.insert(0, "editor %s" % get_editor()) | |
6328099f | 724 | |
149bc23e AS |
725 | # Endless interpret loop |
726 | while True: | |
727 | try: | |
728 | c.cmdloop() | |
729 | except KeyboardInterrupt: | |
730 | print("") | |
731 | ||
732 | if __name__ == '__main__': | |
733 | main() |