fix comment counting
[jan-pona-mute.git] / jan-pona-mute.py
1 #!/usr/bin/env python3
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
18 import subprocess
19 import argparse
20 import shutil
21 import cmd
22 import sys
23 import os
24
25 # Command abbreviations
26 _ABBREVS = {
27 "q": "quit",
28 "p": "print",
29 "c": "comments",
30 "r": "reload",
31 "n": "notifications",
32 }
33
34 _RC_PATHS = (
35 "~/.config/jan-pona-mute/login",
36 "~/.config/.jan-pona-mute",
37 "~/.jan-pona-mute"
38 )
39
40 _PAGERS = (
41 "mdcat",
42 "fold",
43 "cat"
44 )
45
46 # Init file finder
47 def get_rcfile():
48 for rc_path in _RC_PATHS:
49 rcfile = os.path.expanduser(rc_path)
50 if os.path.exists(rcfile):
51 return rcfile
52 return None
53
54 # Pager finder
55 def get_pager():
56 for cmd in _PAGERS:
57 pager = shutil.which(cmd)
58 if pager != None:
59 return pager
60
61 class DiasporaClient(cmd.Cmd):
62
63 prompt = "\x1b[38;5;255m" + "> " + "\x1b[0m"
64 intro = "Welcome to Diaspora! Use the intro command for a quick introduction."
65
66 header_format = "\x1b[1;38;5;255m" + "%s" + "\x1b[0m"
67
68 username = None
69 pod = None
70 password = None
71 pager = None
72
73 connection = None
74 notifications = []
75 index = None
76 post = None
77 post_cache = {} # key is self.post.uid, and notification.id
78
79 undo = []
80
81
82 # dict mapping user ids to usernames
83 users = {}
84
85 def get_username(self, guid):
86 if guid in self.users:
87 return self.users[guid]
88 else:
89 user = diaspy.people.User(connection = self.connection, guid = guid)
90 self.users[guid] = user.handle()
91 return self.users[guid]
92
93 def do_intro(self, line):
94 """Start here."""
95 print("""
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
99 file.
100
101 Once you've listed things such as notifications, enter a number to
102 select the corresponding item.
103 """)
104
105 def do_account(self, account):
106 """Set username and pod using the format username@pod."""
107 try:
108 (self.username, self.pod) = account.split('@')
109 print("Username and pod set: %s@%s" % (self.username, self.pod))
110 except ValueError:
111 print("The account must contain an @ character, e.g. kensanata@pluspora.com.")
112 print("Use the account comand to set the account.")
113
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)
121
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"))
126
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")
133 else:
134 rcfile = get_rcfile()
135 if rcfile == None:
136 rfile = first(_RC_PATHS)
137 seen_account = False
138 seen_password = False
139 seen_login = False
140 changed = False
141 file = []
142 with open(rcfile, "r") as fp:
143 for line in fp:
144 words = line.strip().split()
145 if words:
146 if words[0] == "account":
147 seen_account = True
148 account = "%s@%s" % (self.username, self.pod)
149 if len(words) > 1 and words[1] != account:
150 line = "account %s\n" % account
151 changed = True
152 elif words[0] == "password":
153 seen_password = True
154 if len(words) > 1 and words[1] != self.password:
155 line = "password %s\n" % self.password
156 changed = True
157 elif words[0] == "login":
158 if seen_account and seen_password:
159 seen_login = True
160 else:
161 # skip login if no account or no password given
162 line = None
163 changed = True
164 if line != None:
165 file.append(line)
166 if not seen_account:
167 file.append("account %s@%s\n" % (self.username, self.pod))
168 changed = True
169 if not seen_password:
170 file.append("password %s\n" % self.password)
171 changed = True
172 if not seen_login:
173 file.append("login\n")
174 changed = True
175 if changed:
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)
183 else:
184 print("No changes made, %s left unchanged" % rcfile)
185
186 def do_login(self, line):
187 """Login."""
188 if 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")
194 else:
195 self.connection = diaspy.connection.Connection(
196 pod = "https://%s" % self.pod, username = self.username, password = self.password)
197 try:
198 self.connection.login()
199 self.onecmd("notifications")
200 except diaspy.errors.LoginError:
201 print("Login failed")
202
203 def do_pager(self, pager):
204 """Set the pager, e.g. to cat"""
205 self.pager = pager
206 print("Pager set: %s" % self.pager)
207
208 def header(self, line):
209 """Wrap line in header format."""
210 return self.header_format % line
211
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.")
220 return
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.")
226 else:
227 print("There are no notifications. 😢")
228
229 ### The end!
230 def do_quit(self, *args):
231 """Exit jan-pona-mute."""
232 print("Be safe!")
233 sys.exit()
234
235 def default(self, line):
236 if line.strip() == "EOF":
237 return self.onecmd("quit")
238
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)
245
246 # Finally, see if it's a notification and show it
247 self.do_show(line)
248
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.")
254 return
255 if line == "":
256 print("The 'show' command takes a notification number")
257 return
258 try:
259 n = int(line.strip())
260 notification = self.notifications[n-1]
261 self.index = n
262 except ValueError:
263 print("The 'show' command takes a notification number but '%s' is not a number" % line)
264 return
265 except IndexError:
266 print("Index too high!")
267 return
268
269 self.show(notification)
270 self.load(notification.about())
271
272 print()
273 self.show(self.post)
274
275 if(self.post.comments):
276 print()
277 if len(self.post.comments) == 1:
278 print("There is 1 comment.")
279 else:
280 print("There are %d comments." % len(self.post.comments))
281 print("Use the 'comments' command to list the latest comments.")
282
283 def load(self, id):
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")
289 else:
290 print("Loading...")
291 self.post = diaspy.models.Post(connection = self.connection, id = id)
292 self.post_cache[id] = self.post
293 return self.post
294
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.")
299 return
300 print("Reloading...")
301 self.post = diaspy.models.Post(connection = self.connection, id = self.post.id)
302 self.post_cache[id] = self.post
303
304 def show(self, item):
305 """Show the current item."""
306 if self.pager:
307 subprocess.run(self.pager, input = str(item), text = True)
308 else:
309 print(str(item))
310
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.")
317 return
318 if self.post.comments == None:
319 print("The current post has no comments.")
320 return
321
322 n = 5
323 comments = self.post.comments
324
325 if line == "all":
326 n = None
327 elif line != "":
328 try:
329 n = int(line.strip())
330 except ValueError:
331 print("The 'comments' command takes a number as its argument, or 'all'")
332 print("The default is to show the last 5 comments")
333 return
334
335 if n != None:
336 comments = comments[-n:]
337
338 if comments:
339 for n, comment in enumerate(comments):
340 print()
341 print(self.header("%2d. %s %s") % (n+1, comment.when(), comment.author()))
342 print()
343 self.show(comment)
344 else:
345 print("There are no comments on the selected post.")
346
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.")
351 return
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.")
356
357 def do_delete(self, line):
358 """Delete a comment."""
359 words = line.strip().split()
360 if words:
361 if words[0] == "comment":
362 if self.post == None:
363 print("Use the 'show' command to show a post, first.")
364 return
365 if len(words) != 4:
366 print("Deleting a comment requires a comment id and a post id.")
367 print("delete comment <comment id> from <post id>")
368 return
369 self.post_cache[words[3]].delete_comment(words[1])
370 print("Comment deleted.")
371 else:
372 print("Deleting '%s' is not supported." % words[0])
373 return
374 else:
375 print("Delete what?")
376
377 def do_undo(self, line):
378 """Undo an action."""
379 if line != "":
380 print("Undo does not take an argument.")
381 return
382 if not self.undo:
383 print("There is nothing to undo.")
384 return
385 return self.onecmd(self.undo.pop())
386
387 # Main function
388 def main():
389
390 # Parse args
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()
395
396 # Instantiate client
397 c = DiasporaClient()
398
399 # Process init file
400 seen_pager = False
401 if args.init_file:
402 rcfile = get_rcfile()
403 if rcfile:
404 print("Using init file %s" % rcfile)
405 with open(rcfile, "r") as fp:
406 for line in fp:
407 line = line.strip()
408 if line != "":
409 c.cmdqueue.append(line)
410 if not seen_pager:
411 seen_pager = line.startswith("pager ");
412 else:
413 print("Use the 'save' command to save your login sequence to an init file")
414
415 if not seen_pager:
416 # prepend
417 c.cmdqueue.insert(0, "pager %s" % get_pager())
418
419 # Endless interpret loop
420 while True:
421 try:
422 c.cmdloop()
423 except KeyboardInterrupt:
424 print("")
425
426 if __name__ == '__main__':
427 main()