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
35 "~/.config/jan-pona-mute/login",
36 "~/.config/.jan-pona-mute",
48 for rc_path
in _RC_PATHS
:
49 rcfile
= os
.path
.expanduser(rc_path
)
50 if os
.path
.exists(rcfile
):
57 pager
= shutil
.which(cmd
)
61 class DiasporaClient(cmd
.Cmd
):
63 prompt
= "\x1b[38;5;255m" + "> " + "\x1b[0m"
64 intro
= "Welcome to Diaspora! Use the intro command for a quick introduction."
66 header_format
= "\x1b[1;38;5;255m" + "%s" + "\x1b[0m"
77 post_cache
= {} # key is self.post.uid, and notification.id
82 # dict mapping user ids to usernames
85 def get_username(self
, guid
):
86 if guid
in self
.users
:
87 return self
.users
[guid
]
89 user
= diaspy
.people
.User(connection
= self
.connection
, guid
= guid
)
90 self
.users
[guid
] = user
.handle()
91 return self
.users
[guid
]
93 def do_intro(self
, line
):
96 Use the 'account' and 'password' commands to set up your connection,
97 then use the 'login' command to log in. If everything works as
98 intended, use the 'save' command to save these commands to an init
101 Once you've listed things such as notifications, enter a number to
102 select the corresponding item.
105 def do_account(self
, account
):
106 """Set username and pod using the format username@pod."""
108 (self
.username
, self
.pod
) = account
.split('@')
109 print("Username and pod set: %s@%s" % (self
.username
, self
.pod
))
111 print("The account must contain an @ character, e.g. kensanata@pluspora.com.")
112 print("Use the account comand to set the account.")
114 def do_info(self
, line
):
115 """Get some info about things. By default, it is info about yourself."""
116 print("Info about yourself:")
117 print("Username: %s" % self
.username
)
118 print("Password: %s" % ("None" if self
.password
== None else "set"))
119 print("Pod: %s" % self
.pod
)
120 print("Pager: %s" % self
.pager
)
122 def do_password(self
, password
):
123 """Set the password."""
124 self
.password
= (None if self
.password
== "" else password
)
125 print("Password %s" % ("unset" if self
.password
== "" else "set"))
127 def do_save(self
, line
):
128 """Save your login information to the init file."""
129 if self
.username
== None or self
.pod
== None:
130 print("Use the 'account' command to set username and pod")
131 elif self
.password
== None:
132 print("Use the 'password' command")
134 rcfile
= get_rcfile()
136 rfile
= first(_RC_PATHS
)
138 seen_password
= False
142 with
open(rcfile
, "r") as fp
:
144 words
= line
.strip().split()
146 if words
[0] == "account":
148 account
= "%s@%s" % (self
.username
, self
.pod
)
149 if len(words
) > 1 and words
[1] != account
:
150 line
= "account %s\n" % account
152 elif words
[0] == "password":
154 if len(words
) > 1 and words
[1] != self
.password
:
155 line
= "password %s\n" % self
.password
157 elif words
[0] == "login":
158 if seen_account
and seen_password
:
161 # skip login if no account or no password given
167 file.append("account %s@%s\n" % (self
.username
, self
.pod
))
169 if not seen_password
:
170 file.append("password %s\n" % self
.password
)
173 file.append("login\n")
176 if os
.path
.isfile(rcfile
):
177 os
.rename(rcfile
, rcfile
+ "~")
178 if not os
.path
.isdir(os
.path
.dirname(rcfile
)):
179 os
.makedirs(os
.path
.dirname(rcfile
))
180 with
open(rcfile
, "w") as fp
:
181 fp
.write("".join(file))
182 print("Wrote %s" % rcfile
)
184 print("No changes made, %s left unchanged" % rcfile
)
186 def do_login(self
, line
):
189 self
.onecmd("account %s" % line
)
190 if self
.username
== None or self
.pod
== None:
191 print("Use the 'account' command to set username and pod")
192 elif self
.password
== None:
193 print("Use the 'password' command")
195 self
.connection
= diaspy
.connection
.Connection(
196 pod
= "https://%s" % self
.pod
, username
= self
.username
, password
= self
.password
)
198 self
.connection
.login()
199 self
.onecmd("notifications")
200 except diaspy
.errors
.LoginError
:
201 print("Login failed")
203 def do_pager(self
, pager
):
204 """Set the pager, e.g. to cat"""
206 print("Pager set: %s" % self
.pager
)
208 def header(self
, line
):
209 """Wrap line in header format."""
210 return self
.header_format
% line
212 def do_notifications(self
, line
):
213 """List notifications. Use 'notifications reload' to reload them."""
214 if line
== "" and self
.notifications
:
215 print("Redisplaying the notifications in the cache.")
216 print("Use the 'reload' argument to reload them.")
217 elif line
== "reload" or not self
.notifications
:
218 if self
.connection
== None:
219 print("Use the 'login' command, first.")
221 self
.notifications
= diaspy
.notifications
.Notifications(self
.connection
).last()
222 if self
.notifications
:
223 for n
, notification
in enumerate(self
.notifications
):
224 print(self
.header("%2d. %s %s") % (n
+1, notification
.when(), notification
))
225 print("Enter a number to select the notification.")
227 print("There are no notifications. 😢")
230 def do_quit(self
, *args
):
231 """Exit jan-pona-mute."""
235 def default(self
, line
):
236 if line
.strip() == "EOF":
237 return self
.onecmd("quit")
239 # Expand abbreviated commands
240 first_word
= line
.split()[0].strip()
241 if first_word
in _ABBREVS
:
242 full_cmd
= _ABBREVS
[first_word
]
243 expanded
= line
.replace(first_word
, full_cmd
, 1)
244 return self
.onecmd(expanded
)
246 # Finally, see if it's a notification and show it
249 def do_show(self
, line
):
250 """Show the post given by the index number.
251 The index number must refer to the current list of notifications."""
252 if not self
.notifications
:
253 print("No notifications were loaded.")
256 print("The 'show' command takes a notification number")
259 n
= int(line
.strip())
260 notification
= self
.notifications
[n
-1]
263 print("The 'show' command takes a notification number but '%s' is not a number" % line
)
266 print("Index too high!")
269 self
.show(notification
)
270 self
.load(notification
.about())
275 if(self
.post
.comments
):
277 if len(self
.post
.comments
) == 1:
278 print("There is 1 comment.")
280 print("There are %d comments." % len(self
.post
.comments
))
281 print("Use the 'comments' command to list the latest comments.")
284 """Load the post belonging to the id (from a notification),
285 or get it from the cache."""
286 if id in self
.post_cache
:
287 self
.post
= self
.post_cache
[id]
288 print("Retrieved post from the cache")
291 self
.post
= diaspy
.models
.Post(connection
= self
.connection
, id = id)
292 self
.post_cache
[id] = self
.post
295 def do_reload(self
, line
):
296 """Reload the current post."""
297 if self
.post
== None:
298 print("Use the 'show' command to show a post, first.")
300 print("Reloading...")
301 self
.post
= diaspy
.models
.Post(connection
= self
.connection
, id = self
.post
.id)
302 self
.post_cache
[id] = self
.post
304 def show(self
, item
):
305 """Show the current item."""
307 subprocess
.run(self
.pager
, input = str(item
), text
= True)
311 def do_comments(self
, line
):
312 """Show the comments for the current post.
313 Use the 'all' argument to show them all. Use a numerical argument to
314 show that many. The default is to load the last five."""
315 if self
.post
== None:
316 print("Use the 'show' command to show a post, first.")
318 if self
.post
.comments
== None:
319 print("The current post has no comments.")
323 comments
= self
.post
.comments
329 n
= int(line
.strip())
331 print("The 'comments' command takes a number as its argument, or 'all'")
332 print("The default is to show the last 5 comments")
336 comments
= comments
[-n
:]
339 for n
, comment
in enumerate(comments
):
341 print(self
.header("%2d. %s %s") % (n
+1, comment
.when(), comment
.author()))
345 print("There are no comments on the selected post.")
347 def do_comment(self
, line
):
348 """Leave a comment on the current post."""
349 if self
.post
== None:
350 print("Use the 'show' command to show a post, first.")
352 comment
= self
.post
.comment(line
)
353 self
.post
.comments
.add(comment
)
354 self
.undo
.append("delete comment %s from %s" % (comment
.id, self
.id))
355 print("Comment posted.")
357 def do_delete(self
, line
):
358 """Delete a comment."""
359 words
= line
.strip().split()
361 if words
[0] == "comment":
362 if self
.post
== None:
363 print("Use the 'show' command to show a post, first.")
366 print("Deleting a comment requires a comment id and a post id.")
367 print("delete comment <comment id> from <post id>")
369 self
.post_cache
[words
[3]].delete_comment(words
[1])
370 print("Comment deleted.")
372 print("Deleting '%s' is not supported." % words
[0])
375 print("Delete what?")
377 def do_undo(self
, line
):
378 """Undo an action."""
380 print("Undo does not take an argument.")
383 print("There is nothing to undo.")
385 return self
.onecmd(self
.undo
.pop())
391 parser
= argparse
.ArgumentParser(description
='A command line Diaspora client.')
392 parser
.add_argument('--no-init-file', dest
='init_file', action
='store_const',
393 const
=False, default
=True, help='Do not load a init file')
394 args
= parser
.parse_args()
402 rcfile
= get_rcfile()
404 print("Using init file %s" % rcfile
)
405 with
open(rcfile
, "r") as fp
:
409 c
.cmdqueue
.append(line
)
411 seen_pager
= line
.startswith("pager ");
413 print("Use the 'save' command to save your login sequence to an init file")
417 c
.cmdqueue
.insert(0, "pager %s" % get_pager())
419 # Endless interpret loop
423 except KeyboardInterrupt:
426 if __name__
== '__main__':