dfe13cdb92e4025c67e37213af37f4e9e4e43b03
2 # Copyright (C) 2019 Alex Schroeder <alex@gnu.org>
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
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
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/>.
25 # Command abbreviations
36 "~/.config/jan-pona-mute/login",
37 "~/.jan-pona-mute.d/login"
42 "~/.config/jan-pona-mute/notes",
43 "~/.jan-pona-mute.d/notes"
60 """Init file finder"""
61 for path
in _RC_PATHS
:
62 path
= os
.path
.expanduser(path
)
63 if os
.path
.exists(path
):
68 """Notes directory finder"""
70 for path
in _NOTE_DIRS
:
71 path
= os
.path
.expanduser(path
)
72 if os
.path
.isdir(path
):
76 dir = os
.path
.expanduser(_NOTE_DIRS
[0])
77 if not os
.path
.isdir(dir):
84 bin
= shutil
.which(cmd
)
90 return get_binary(_PAGERS
)
94 return get_binary(_EDITORS
)
96 class DiasporaClient(cmd
.Cmd
):
98 prompt
= "\x1b[38;5;255m" + "> " + "\x1b[0m"
99 intro
= "Welcome to Diaspora! Use the intro command for a quick introduction."
101 header_format
= "\x1b[1;38;5;255m" + "%s" + "\x1b[0m"
113 post_cache
= {} # key is self.post.uid, and notification.id
118 # dict mapping user ids to usernames
121 def get_username(self
, guid
):
122 if guid
in self
.users
:
123 return self
.users
[guid
]
125 user
= diaspy
.people
.User(connection
= self
.connection
, guid
= guid
)
126 self
.users
[guid
] = user
.handle()
127 return self
.users
[guid
]
129 def do_intro(self
, line
):
132 Use the 'account' and 'password' commands to set up your connection,
133 then use the 'login' command to log in. If everything works as
134 intended, use the 'save' command to save these commands to an init
137 Once you've listed things such as notifications, enter a number to
138 select the corresponding item.
141 def do_account(self
, account
):
142 """Set username and pod using the format username@pod."""
144 (self
.username
, self
.pod
) = account
.split('@')
145 print("Username and pod set: %s@%s" % (self
.username
, self
.pod
))
147 print("The account must contain an @ character, e.g. kensanata@pluspora.com.")
148 print("Use the account comand to set the account.")
150 def do_info(self
, line
):
151 """Get some info about things. By default, it is info about yourself."""
152 print("Info about yourself:")
153 print("Username: %s" % self
.username
)
154 print("Password: %s" % ("None" if self
.password
== None else "set"))
155 print("Pod: %s" % self
.pod
)
156 print("Pager: %s" % self
.pager
)
157 print("Editor: %s" % self
.editor
)
159 def do_password(self
, password
):
160 """Set the password."""
161 self
.password
= (None if self
.password
== "" else password
)
162 print("Password %s" % ("unset" if self
.password
== "" else "set"))
164 def do_save(self
, line
):
165 """Save your login information to the init file."""
166 if self
.username
== None or self
.pod
== None:
167 print("Use the 'account' command to set username and pod.")
168 elif self
.password
== None:
169 print("Use the 'password' command.")
171 rcfile
= get_rcfile()
175 seen_password
= False
179 with
open(rcfile
, "r") as fp
:
181 words
= line
.strip().split()
183 if words
[0] == "account":
185 account
= "%s@%s" % (self
.username
, self
.pod
)
186 if len(words
) > 1 and words
[1] != account
:
187 line
= "account %s\n" % account
189 elif words
[0] == "password":
191 if len(words
) > 1 and words
[1] != self
.password
:
192 line
= "password %s\n" % self
.password
194 elif words
[0] == "login":
195 if seen_account
and seen_password
:
198 # skip login if no account or no password given
204 file.append("account %s@%s\n" % (self
.username
, self
.pod
))
206 if not seen_password
:
207 file.append("password %s\n" % self
.password
)
210 file.append("login\n")
213 if os
.path
.isfile(rcfile
):
214 os
.rename(rcfile
, rcfile
+ "~")
215 if not os
.path
.isdir(os
.path
.dirname(rcfile
)):
216 os
.makedirs(os
.path
.dirname(rcfile
))
217 with
open(rcfile
, "w") as fp
:
218 fp
.write("".join(file))
219 print("Wrote %s" % rcfile
)
221 print("No changes made, %s left unchanged" % rcfile
)
223 def do_login(self
, line
):
226 self
.onecmd("account %s" % line
)
227 if self
.username
== None or self
.pod
== None:
228 print("Use the 'account' command to set username and pod.")
229 elif self
.password
== None:
230 print("Use the 'password' command.")
232 self
.connection
= diaspy
.connection
.Connection(
233 pod
= "https://%s" % self
.pod
, username
= self
.username
, password
= self
.password
)
235 self
.connection
.login()
236 self
.onecmd("notifications")
237 except diaspy
.errors
.LoginError
:
238 print("Login failed.")
240 def do_pager(self
, pager
):
241 """Set the pager, e.g. to cat"""
243 print("Pager set: %s" % self
.pager
)
245 def do_editor(self
, editor
):
246 """Set the editor, e.g. to ed"""
248 print("Editor set: %s" % self
.editor
)
250 def header(self
, line
):
251 """Wrap line in header format."""
252 return self
.header_format
% line
254 def do_notifications(self
, line
):
255 """List notifications. Use 'notifications reload' to reload them."""
256 if line
== "" and self
.notifications
:
257 print("Redisplaying the notifications in the cache.")
258 print("Use the 'reload' argument to reload them.")
259 elif line
== "reload" or not self
.notifications
:
260 if self
.connection
== None:
261 print("Use the 'login' command, first.")
263 self
.notifications
= diaspy
.notifications
.Notifications(self
.connection
).last()
265 print("The 'notifications' command only takes the optional argument 'reload'.")
267 if self
.notifications
:
268 for n
, notification
in enumerate(self
.notifications
):
269 print(self
.header("%2d. %s %s") % (n
+1, notification
.when(), notification
))
270 print("Enter a number to select the notification.")
272 print("There are no notifications. 😢")
275 def do_quit(self
, *args
):
276 """Exit jan-pona-mute."""
280 def default(self
, line
):
281 if line
.strip() == "EOF":
282 return self
.onecmd("quit")
284 # Expand abbreviated commands
285 first_word
= line
.split()[0].strip()
286 if first_word
in _ABBREVS
:
287 full_cmd
= _ABBREVS
[first_word
]
288 expanded
= line
.replace(first_word
, full_cmd
, 1)
289 return self
.onecmd(expanded
)
291 # Finally, see if it's a notification and show it
294 def do_show(self
, line
):
295 """Show the post given by the index number.
296 The index number must refer to the current list of notifications."""
297 if not self
.notifications
:
298 print("No notifications were loaded.")
301 print("The 'show' command takes a notification number.")
304 n
= int(line
.strip())
305 notification
= self
.notifications
[n
-1]
308 print("The 'show' command takes a notification number but '%s' is not a number" % line
)
311 print("Index too high!")
314 self
.show(notification
)
315 self
.load(notification
.about())
320 if(self
.post
.comments
):
322 if len(self
.post
.comments
) == 1:
323 print("There is 1 comment.")
325 print("There are %d comments." % len(self
.post
.comments
))
326 print("Use the 'comments' command to list the latest comments.")
329 """Load the post belonging to the id (from a notification),
330 or get it from the cache."""
331 if id in self
.post_cache
:
332 self
.post
= self
.post_cache
[id]
333 print("Retrieved post from the cache.")
336 self
.post
= diaspy
.models
.Post(connection
= self
.connection
, id = id)
337 self
.post_cache
[id] = self
.post
340 def do_reload(self
, line
):
341 """Reload the current post."""
342 if self
.post
== None:
343 print("Use the 'show' command to show a post, first.")
345 print("Reloading...")
346 self
.post
= diaspy
.models
.Post(connection
= self
.connection
, id = self
.post
.id)
347 self
.post_cache
[id] = self
.post
349 def show(self
, item
):
350 """Show the current item."""
352 subprocess
.run(self
.pager
, input = str(item
), text
= True)
356 def do_comments(self
, line
):
357 """Show the comments for the current post.
358 Use the 'all' argument to show them all. Use a numerical argument to
359 show that many. The default is to load the last five."""
360 if self
.post
== None:
361 print("Use the 'show' command to show a post, first.")
363 if self
.post
.comments
== None:
364 print("The current post has no comments.")
368 comments
= self
.post
.comments
374 n
= int(line
.strip())
376 print("The 'comments' command takes a number as its argument, or 'all'.")
377 print("The default is to show the last 5 comments.")
384 start
= max(len(comments
) - n
, 0)
387 for n
, comment
in enumerate(comments
[start
:], start
):
389 print(self
.header("%2d. %s %s") % (n
+1, comment
.when(), comment
.author()))
393 print("There are no comments on the selected post.")
395 def do_comment(self
, line
):
396 """Leave a comment on the current post.
397 If you just use a number as your comment, it will refer to a note.
398 Use the 'edit' command to edit notes."""
399 if self
.post
== None:
400 print("Use the 'show' command to show a post, first.")
403 # if the comment is just a number, use a note to post
404 n
= int(line
.strip())
405 notes
= self
.get_notes()
408 with
open(self
.get_note_path(notes
[n
-1]), mode
= 'r', encoding
= 'utf-8') as fp
:
410 print("Using note #%d: %s" % (n
, notes
[n
-1]))
412 print("Use the 'list notes' command to list valid numbers.")
415 print("There are no notes to use.")
418 # in which case we'll simply comment with the line
420 comment
= self
.post
.comment(line
)
421 self
.post
.comments
.add(comment
)
422 self
.undo
.append("delete comment %s from %s" % (comment
.id, self
.post
.id))
423 print("Comment posted.")
425 def do_delete(self
, line
):
426 """Delete a comment."""
427 words
= line
.strip().split()
429 if words
[0] == "comment":
430 if self
.post
== None:
431 print("Use the 'show' command to show a post, first.")
434 self
.post_cache
[words
[3]].delete_comment(words
[1])
435 print("Comment deleted.")
440 comment
= self
.post
.comments
[n
-1]
443 print("Deleting a comment requires an integer.")
446 print("Use the 'comments' command to find valid comment numbers.")
448 # not catching any errors from diaspy
449 self
.post
.delete_comment(id)
450 # there is no self.post.comments.remove(id)
451 comments
= [c
.id for c
in self
.post
.comments
if c
.id != id]
452 self
.post
.comments
= diaspy
.models
.Comments(comments
)
453 print("Comment deleted.")
456 print("Deleting a comment requires a comment id and a post id, or a number.")
457 print("delete comment <comment id> from <post id>")
458 print("delete comment 5")
460 if words
[0] == "note":
462 print("Deleting a note requires a number.")
467 print("Deleting a note requires an integer.")
469 notes
= self
.get_notes()
472 os
.unlink(self
.get_note_path(notes
[n
-1]))
473 print("Deleted note #%d: %s" % (n
, notes
[n
-1]))
475 print("Use the 'list notes' command to list valid numbers.")
477 print("There are no notes to delete.")
479 print("Things to delete: comment, note.")
482 print("Delete what?")
484 def do_undo(self
, line
):
485 """Undo an action."""
487 print("The 'undo' command does not take an argument.")
490 print("There is nothing to undo.")
492 return self
.onecmd(self
.undo
.pop())
494 def do_edit(self
, line
):
495 """Edit a note with a given name."""
497 print("Edit takes the name of a note as an argument.")
499 file = self
.get_note_path(line
)
501 subprocess
.run([self
.editor
, file])
504 print("Use the 'editor' command to set an editor.")
506 def do_notes(self
, line
):
509 print("The 'notes' command does not take an argument.")
511 notes
= self
.get_notes()
513 for n
, note
in enumerate(notes
):
514 print(self
.header("%2d. %s") % (n
+1, note
))
516 print("Use 'edit' to create a note.")
518 print("Things to list: notes.")
521 """Get the list of notes."""
522 return os
.listdir(get_notes_dir())
524 def get_note_path(self
, filename
):
525 """Get the correct path for a note."""
526 return os
.path
.join(get_notes_dir(), filename
)
532 parser
= argparse
.ArgumentParser(description
='A command line Diaspora client.')
533 parser
.add_argument('--no-init-file', dest
='init_file', action
='store_const',
534 const
=False, default
=True, help='Do not load a init file')
535 args
= parser
.parse_args()
544 rcfile
= get_rcfile()
546 print("Using init file %s" % rcfile
)
547 with
open(rcfile
, "r") as fp
:
551 c
.cmdqueue
.append(line
)
553 seen_pager
= line
.startswith("pager ");
555 seen_editor
= line
.startswith("editor ");
557 print("Use the 'save' command to save your login sequence to an init file.")
561 c
.cmdqueue
.insert(0, "pager %s" % get_pager())
564 c
.cmdqueue
.insert(0, "editor %s" % get_editor())
566 # Endless interpret loop
570 except KeyboardInterrupt:
573 if __name__
== '__main__':