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