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 | 234 | else: |
1860d749 | 235 | print("Setting up a connection...") |
1882b5bc AS |
236 | self.connection = diaspy.connection.Connection( |
237 | pod = "https://%s" % self.pod, username = self.username, password = self.password) | |
238 | try: | |
1860d749 | 239 | print("Logging in...") |
1882b5bc | 240 | self.connection.login() |
1882b5bc | 241 | except diaspy.errors.LoginError: |
ebc699fd | 242 | print("Login failed.") |
149bc23e | 243 | |
6328099f AS |
244 | def do_pager(self, pager): |
245 | """Set the pager, e.g. to cat""" | |
246 | self.pager = pager | |
247 | print("Pager set: %s" % self.pager) | |
248 | ||
ebc699fd AS |
249 | def do_editor(self, editor): |
250 | """Set the editor, e.g. to ed""" | |
251 | self.editor = editor | |
252 | print("Editor set: %s" % self.editor) | |
253 | ||
8bf63019 AS |
254 | def header(self, line): |
255 | """Wrap line in header format.""" | |
256 | return self.header_format % line | |
257 | ||
149bc23e | 258 | def do_notifications(self, line): |
bb8316e5 AS |
259 | """List notifications. Use 'notifications reload' to reload them.""" |
260 | if line == "" and self.notifications: | |
261 | print("Redisplaying the notifications in the cache.") | |
a4b8710c | 262 | print("Use 'notifications reload' to reload them.") |
bb8316e5 AS |
263 | elif line == "reload" or not self.notifications: |
264 | if self.connection == None: | |
265 | print("Use the 'login' command, first.") | |
266 | return | |
267 | self.notifications = diaspy.notifications.Notifications(self.connection).last() | |
ebc699fd AS |
268 | else: |
269 | print("The 'notifications' command only takes the optional argument 'reload'.") | |
270 | return | |
bb8316e5 AS |
271 | if self.notifications: |
272 | for n, notification in enumerate(self.notifications): | |
82f0d747 AS |
273 | if notification.unread: |
274 | print(self.header("%2d. %s %s") % (n+1, notification.when(), notification)) | |
275 | else: | |
276 | print("%2d. %s %s" % (n+1, notification.when(), notification)) | |
bb8316e5 | 277 | print("Enter a number to select the notification.") |
bec46251 | 278 | self.numbers_refer_to = 'notifications' |
bb8316e5 AS |
279 | else: |
280 | print("There are no notifications. 😢") | |
149bc23e AS |
281 | |
282 | ### The end! | |
283 | def do_quit(self, *args): | |
284 | """Exit jan-pona-mute.""" | |
285 | print("Be safe!") | |
286 | sys.exit() | |
287 | ||
288 | def default(self, line): | |
289 | if line.strip() == "EOF": | |
290 | return self.onecmd("quit") | |
291 | ||
292 | # Expand abbreviated commands | |
293 | first_word = line.split()[0].strip() | |
82f0d747 AS |
294 | if first_word in shortcuts: |
295 | full_cmd = shortcuts[first_word] | |
149bc23e AS |
296 | expanded = line.replace(first_word, full_cmd, 1) |
297 | return self.onecmd(expanded) | |
298 | ||
bb8316e5 AS |
299 | # Finally, see if it's a notification and show it |
300 | self.do_show(line) | |
1882b5bc | 301 | |
bb8316e5 | 302 | def do_show(self, line): |
18e74209 | 303 | """Show the post given by the index number. |
bec46251 | 304 | The index number must refer to the current list of notifications |
1ff8fa2c AS |
305 | or the home stream. If no index number is given, show the current |
306 | post again.""" | |
bec46251 AS |
307 | if not self.notifications and not self.home: |
308 | print("Use the 'login' command to load notifications.") | |
bb8316e5 | 309 | return |
1ff8fa2c | 310 | if line == "" and self.post == None: |
bec46251 | 311 | print("Please specify a number.") |
bb8316e5 | 312 | return |
1ff8fa2c AS |
313 | if line != "": |
314 | try: | |
315 | n = int(line.strip()) | |
316 | if self.numbers_refer_to == 'notifications': | |
317 | notification = self.notifications[n-1] | |
318 | self.show(notification) | |
319 | self.load(notification.about()) | |
320 | elif self.numbers_refer_to == 'home': | |
fac4e3ca AS |
321 | posts = sorted(self.home, key=lambda x: x.data()["created_at"]) |
322 | self.post = posts[n-1] | |
1ff8fa2c AS |
323 | else: |
324 | print("Internal error: not sure what numbers '%s' refer to." % self.numbers_refer_to) | |
325 | return | |
326 | except ValueError: | |
327 | print("The 'show' command takes a notification number but '%s' is not a number" % line) | |
328 | return | |
329 | except IndexError: | |
330 | print("Index too high!") | |
bec46251 | 331 | return |
1882b5bc | 332 | |
18e74209 AS |
333 | print() |
334 | self.show(self.post) | |
1882b5bc | 335 | |
bb8316e5 AS |
336 | if(self.post.comments): |
337 | print() | |
88c1a828 AS |
338 | if len(self.post.comments) == 1: |
339 | print("There is 1 comment.") | |
340 | else: | |
341 | print("There are %d comments." % len(self.post.comments)) | |
bb8316e5 AS |
342 | print("Use the 'comments' command to list the latest comments.") |
343 | ||
344 | def load(self, id): | |
345 | """Load the post belonging to the id (from a notification), | |
346 | or get it from the cache.""" | |
347 | if id in self.post_cache: | |
348 | self.post = self.post_cache[id] | |
ebc699fd | 349 | print("Retrieved post from the cache.") |
bb8316e5 AS |
350 | else: |
351 | print("Loading...") | |
352 | self.post = diaspy.models.Post(connection = self.connection, id = id) | |
353 | self.post_cache[id] = self.post | |
354 | return self.post | |
355 | ||
356 | def do_reload(self, line): | |
357 | """Reload the current post.""" | |
358 | if self.post == None: | |
359 | print("Use the 'show' command to show a post, first.") | |
360 | return | |
361 | print("Reloading...") | |
362 | self.post = diaspy.models.Post(connection = self.connection, id = self.post.id) | |
363 | self.post_cache[id] = self.post | |
364 | ||
1882b5bc AS |
365 | def show(self, item): |
366 | """Show the current item.""" | |
6328099f | 367 | if self.pager: |
88c1a828 | 368 | subprocess.run(self.pager, input = str(item), text = True) |
6328099f | 369 | else: |
88c1a828 | 370 | print(str(item)) |
6328099f | 371 | |
18e74209 | 372 | def do_comments(self, line): |
bb8316e5 AS |
373 | """Show the comments for the current post. |
374 | Use the 'all' argument to show them all. Use a numerical argument to | |
375 | show that many. The default is to load the last five.""" | |
18e74209 | 376 | if self.post == None: |
bb8316e5 | 377 | print("Use the 'show' command to show a post, first.") |
18e74209 AS |
378 | return |
379 | if self.post.comments == None: | |
380 | print("The current post has no comments.") | |
381 | return | |
382 | ||
383 | n = 5 | |
384 | comments = self.post.comments | |
385 | ||
386 | if line == "all": | |
387 | n = None | |
388 | elif line != "": | |
6328099f AS |
389 | try: |
390 | n = int(line.strip()) | |
391 | except ValueError: | |
ebc699fd AS |
392 | print("The 'comments' command takes a number as its argument, or 'all'.") |
393 | print("The default is to show the last 5 comments.") | |
6328099f | 394 | return |
6328099f | 395 | |
ebc699fd AS |
396 | if n == None: |
397 | start = 0 | |
398 | else: | |
399 | # n is from the back | |
400 | start = max(len(comments) - n, 0) | |
18e74209 | 401 | |
42716c49 | 402 | if comments: |
ebc699fd | 403 | for n, comment in enumerate(comments[start:], start): |
42716c49 AS |
404 | print() |
405 | print(self.header("%2d. %s %s") % (n+1, comment.when(), comment.author())) | |
406 | print() | |
407 | self.show(comment) | |
408 | else: | |
409 | print("There are no comments on the selected post.") | |
1882b5bc | 410 | |
714a2f67 | 411 | def do_comment(self, line): |
ebc699fd AS |
412 | """Leave a comment on the current post. |
413 | If you just use a number as your comment, it will refer to a note. | |
414 | Use the 'edit' command to edit notes.""" | |
714a2f67 | 415 | if self.post == None: |
bb8316e5 | 416 | print("Use the 'show' command to show a post, first.") |
714a2f67 | 417 | return |
ebc699fd | 418 | try: |
9200e1e1 | 419 | # if the comment is just a number, use a note to post |
ebc699fd AS |
420 | n = int(line.strip()) |
421 | notes = self.get_notes() | |
422 | if notes: | |
423 | try: | |
7e2b259d | 424 | line = self.read_note(notes[n-1]) |
ebc699fd AS |
425 | print("Using note #%d: %s" % (n, notes[n-1])) |
426 | except IndexError: | |
427 | print("Use the 'list notes' command to list valid numbers.") | |
428 | return | |
429 | else: | |
430 | print("There are no notes to use.") | |
431 | return | |
432 | except ValueError: | |
9200e1e1 AS |
433 | # in which case we'll simply comment with the line |
434 | pass | |
714a2f67 AS |
435 | comment = self.post.comment(line) |
436 | self.post.comments.add(comment) | |
ebc699fd | 437 | self.undo.append("delete comment %s from %s" % (comment.id, self.post.id)) |
714a2f67 AS |
438 | print("Comment posted.") |
439 | ||
1ff8fa2c AS |
440 | def do_post(self, line): |
441 | """Write a post on the current stream. | |
442 | If you just use a number as your post, it will refer to a note. | |
443 | Use the 'edit' command to edit notes.""" | |
444 | if self.home == None: | |
445 | self.home = diaspy.streams.Stream(self.connection) | |
446 | try: | |
447 | # if the post is just a number, use a note to post | |
448 | n = int(line.strip()) | |
449 | notes = self.get_notes() | |
450 | if notes: | |
451 | try: | |
452 | line = self.read_note(notes[n-1]) | |
453 | print("Using note #%d: %s" % (n, notes[n-1])) | |
454 | except IndexError: | |
455 | print("Use the 'list notes' command to list valid numbers.") | |
456 | return | |
457 | else: | |
458 | print("There are no notes to use.") | |
459 | return | |
460 | except ValueError: | |
461 | # in which case we'll simply post the line | |
462 | pass | |
463 | self.post = self.home.post(text = line) | |
464 | self.post_cache[self.post.id] = self.post | |
465 | self.undo.append("delete post %s" % self.post.id) | |
466 | print("Posted. Use the 'show' command to show it.") | |
467 | ||
f1bfb7bc AS |
468 | def do_delete(self, line): |
469 | """Delete a comment.""" | |
470 | words = line.strip().split() | |
471 | if words: | |
472 | if words[0] == "comment": | |
ebc699fd | 473 | if len(words) == 4: |
b5d7f34e AS |
474 | post = self.post_cache[words[3]] |
475 | post.delete_comment(words[1]) | |
476 | comments = [c.id for c in post.comments if c.id != id] | |
477 | post.comments = diaspy.models.Comments(comments) | |
ebc699fd AS |
478 | print("Comment deleted.") |
479 | return | |
b5d7f34e AS |
480 | if self.post == None: |
481 | print("Use the 'show' command to show a post, first.") | |
482 | return | |
ebc699fd AS |
483 | if len(words) == 2: |
484 | try: | |
485 | n = int(words[1]) | |
486 | comment = self.post.comments[n-1] | |
487 | id = comment.id | |
488 | except ValueError: | |
489 | print("Deleting a comment requires an integer.") | |
490 | return | |
491 | except IndexError: | |
492 | print("Use the 'comments' command to find valid comment numbers.") | |
493 | return | |
494 | # not catching any errors from diaspy | |
495 | self.post.delete_comment(id) | |
496 | # there is no self.post.comments.remove(id) | |
497 | comments = [c.id for c in self.post.comments if c.id != id] | |
498 | self.post.comments = diaspy.models.Comments(comments) | |
499 | print("Comment deleted.") | |
500 | return | |
501 | else: | |
502 | print("Deleting a comment requires a comment id and a post id, or a number.") | |
bb8316e5 | 503 | print("delete comment <comment id> from <post id>") |
ebc699fd AS |
504 | print("delete comment 5") |
505 | return | |
506 | if words[0] == "note": | |
507 | if len(words) != 2: | |
508 | print("Deleting a note requires a number.") | |
509 | return | |
510 | try: | |
511 | n = int(words[1]) | |
512 | except ValueError: | |
513 | print("Deleting a note requires an integer.") | |
f1bfb7bc | 514 | return |
ebc699fd AS |
515 | notes = self.get_notes() |
516 | if notes: | |
517 | try: | |
518 | os.unlink(self.get_note_path(notes[n-1])) | |
519 | print("Deleted note #%d: %s" % (n, notes[n-1])) | |
520 | except IndexError: | |
521 | print("Use the 'list notes' command to list valid numbers.") | |
522 | else: | |
523 | print("There are no notes to delete.") | |
f1bfb7bc | 524 | else: |
ebc699fd | 525 | print("Things to delete: comment, note.") |
f1bfb7bc AS |
526 | return |
527 | else: | |
528 | print("Delete what?") | |
529 | ||
530 | def do_undo(self, line): | |
531 | """Undo an action.""" | |
532 | if line != "": | |
ebc699fd | 533 | print("The 'undo' command does not take an argument.") |
f1bfb7bc AS |
534 | return |
535 | if not self.undo: | |
536 | print("There is nothing to undo.") | |
537 | return | |
538 | return self.onecmd(self.undo.pop()) | |
539 | ||
ebc699fd AS |
540 | def do_edit(self, line): |
541 | """Edit a note with a given name.""" | |
542 | if line == "": | |
543 | print("Edit takes the name of a note as an argument.") | |
544 | return | |
545 | file = self.get_note_path(line) | |
546 | if self.editor: | |
547 | subprocess.run([self.editor, file]) | |
cc59946e | 548 | self.onecmd("notes") |
ebc699fd AS |
549 | else: |
550 | print("Use the 'editor' command to set an editor.") | |
551 | ||
552 | def do_notes(self, line): | |
553 | """List notes""" | |
554 | if line != "": | |
555 | print("The 'notes' command does not take an argument.") | |
556 | return | |
557 | notes = self.get_notes() | |
558 | if notes: | |
559 | for n, note in enumerate(notes): | |
560 | print(self.header("%2d. %s") % (n+1, note)) | |
1ff8fa2c AS |
561 | print("Use the 'edit' command to edit a note.") |
562 | print("Use the 'preview' command to look at a note.") | |
563 | print("Use the 'post' command to post a note.") | |
564 | print("Use the 'comment' command to post a comment.") | |
ebc699fd | 565 | else: |
1ff8fa2c | 566 | print("Use 'edit' to create a note.") |
ebc699fd AS |
567 | |
568 | def get_notes(self): | |
569 | """Get the list of notes.""" | |
570 | return os.listdir(get_notes_dir()) | |
571 | ||
572 | def get_note_path(self, filename): | |
573 | """Get the correct path for a note.""" | |
574 | return os.path.join(get_notes_dir(), filename) | |
575 | ||
7e2b259d AS |
576 | def read_note(self, filename): |
577 | """Get text of a note.""" | |
578 | with open(self.get_note_path(filename), mode = 'r', encoding = 'utf-8') as fp: | |
579 | return fp.read() | |
580 | ||
f38b656c AS |
581 | def do_preview(self, line): |
582 | """Preview a note using your pager. | |
583 | Use the 'pager' command to set your pager to something like 'mdcat'.""" | |
584 | if line == "": | |
585 | print("The 'preview' command the number of a note as an argument.") | |
586 | print("Use the 'notes' command to list all your notes.") | |
587 | return | |
588 | try: | |
589 | n = int(line.strip()) | |
590 | notes = self.get_notes() | |
591 | if notes: | |
592 | try: | |
593 | self.show(self.read_note(notes[n-1])) | |
594 | except IndexError: | |
595 | print("Use the 'list notes' command to list valid numbers.") | |
596 | return | |
597 | else: | |
598 | print("There are no notes to preview.") | |
599 | return | |
600 | except ValueError: | |
601 | print("The 'preview' command takes a number as its argument.") | |
602 | return | |
603 | ||
bec46251 AS |
604 | def do_home(self, line): |
605 | """Show the main stream containing the combined posts of the | |
606 | followed users and tags and the community spotlights posts if | |
607 | the user enabled those.""" | |
608 | if line == "": | |
609 | if self.home: | |
610 | print("Redisplaying the cached statuses of the home stream.") | |
611 | print("Use the 'reload' argument to reload them.") | |
612 | print("Use the 'all' argument to show them all.") | |
613 | print("Use a number to show only that many.") | |
614 | print("The default is 5.") | |
615 | else: | |
616 | print("Loading...") | |
617 | self.home = diaspy.streams.Stream(self.connection) | |
618 | self.home.fill() | |
619 | for post in self.home: | |
620 | if post.id not in self.post_cache: | |
621 | self.post_cache[post.id] = post | |
622 | elif line == "reload": | |
623 | if self.connection == None: | |
624 | print("Use the 'login' command, first.") | |
625 | return | |
626 | if self.home: | |
627 | print("Reloading...") | |
628 | self.home.update() | |
629 | line = "" | |
630 | else: | |
631 | self.home = diaspy.streams.Stream(self.connection) | |
632 | self.home.fill() | |
633 | ||
634 | n = 5 | |
1ff8fa2c | 635 | posts = sorted(self.home, key=lambda x: x.data()["created_at"]) |
bec46251 AS |
636 | |
637 | if line == "all": | |
638 | n = None | |
639 | elif line != "": | |
640 | try: | |
641 | n = int(line.strip()) | |
642 | except ValueError: | |
643 | print("The 'home' command takes a number as its argument, or 'reload' or 'all'.") | |
644 | print("The default is to show the last 5 posts.") | |
645 | return | |
646 | ||
647 | if n == None: | |
648 | start = 0 | |
649 | else: | |
650 | # n is from the back | |
651 | start = max(len(posts) - n, 0) | |
652 | ||
653 | if posts: | |
654 | for n, post in enumerate(posts[start:], start): | |
655 | print() | |
656 | print(self.header("%2d. %s %s") % (n+1, post.data()["created_at"], post.author())) | |
657 | print() | |
658 | self.show(post) | |
659 | ||
660 | print("Enter a number to select the post.") | |
661 | self.numbers_refer_to = 'home' | |
662 | else: | |
663 | print("The people you follow have nothing to say.") | |
664 | print("The tags you follow are empty. 😢") | |
665 | ||
82f0d747 AS |
666 | def do_shortcuts(self, line): |
667 | """List all shortcuts.""" | |
668 | if line != "": | |
669 | print("The 'shortcuts' command does not take an argument.") | |
670 | return | |
671 | print(self.header("Shortcuts")) | |
672 | for shortcut in sorted(shortcuts): | |
673 | print("%s\t%s" % (shortcut, shortcuts[shortcut])) | |
674 | print("Use the 'shortcut' command to change or add shortcuts.") | |
675 | ||
676 | def do_shortcut(self, line): | |
677 | """Define a new shortcut.""" | |
678 | words = line.strip().split(maxsplit = 1) | |
679 | if len(words) == 0: | |
680 | return self.onecmd("shortcuts") | |
681 | elif len(words) == 1: | |
682 | shortcut = words[0] | |
683 | if shortcut in shortcuts: | |
684 | print("%s\t%s" % (shortcut, shortcuts[shortcut])) | |
685 | else: | |
686 | print("%s is not a shortcut" % shortcut) | |
687 | else: | |
688 | shortcuts[words[0]] = words[1] | |
689 | ||
149bc23e AS |
690 | # Main function |
691 | def main(): | |
692 | ||
693 | # Parse args | |
694 | parser = argparse.ArgumentParser(description='A command line Diaspora client.') | |
1882b5bc AS |
695 | parser.add_argument('--no-init-file', dest='init_file', action='store_const', |
696 | const=False, default=True, help='Do not load a init file') | |
149bc23e AS |
697 | args = parser.parse_args() |
698 | ||
699 | # Instantiate client | |
1882b5bc AS |
700 | c = DiasporaClient() |
701 | ||
702 | # Process init file | |
6328099f | 703 | seen_pager = False |
ebc699fd | 704 | seen_editor = False |
1882b5bc AS |
705 | if args.init_file: |
706 | rcfile = get_rcfile() | |
707 | if rcfile: | |
708 | print("Using init file %s" % rcfile) | |
709 | with open(rcfile, "r") as fp: | |
710 | for line in fp: | |
711 | line = line.strip() | |
6328099f AS |
712 | if line != "": |
713 | c.cmdqueue.append(line) | |
714 | if not seen_pager: | |
715 | seen_pager = line.startswith("pager "); | |
ebc699fd AS |
716 | if not seen_editor: |
717 | seen_editor = line.startswith("editor "); | |
1882b5bc | 718 | else: |
ebc699fd | 719 | print("Use the 'save' command to save your login sequence to an init file.") |
149bc23e | 720 | |
6328099f AS |
721 | if not seen_pager: |
722 | # prepend | |
723 | c.cmdqueue.insert(0, "pager %s" % get_pager()) | |
ebc699fd AS |
724 | if not seen_editor: |
725 | # prepend | |
726 | c.cmdqueue.insert(0, "editor %s" % get_editor()) | |
6328099f | 727 | |
149bc23e AS |
728 | # Endless interpret loop |
729 | while True: | |
730 | try: | |
731 | c.cmdloop() | |
732 | except KeyboardInterrupt: | |
733 | print("") | |
734 | ||
735 | if __name__ == '__main__': | |
736 | main() |