fix comment counting
[jan-pona-mute.git] / jan-pona-mute.py
CommitLineData
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
17import diaspy
6328099f 18import subprocess
149bc23e 19import argparse
6328099f 20import shutil
149bc23e
AS
21import cmd
22import sys
23import os
24
25# Command abbreviations
26_ABBREVS = {
27 "q": "quit",
6328099f 28 "p": "print",
1badc74e 29 "c": "comments",
bb8316e5
AS
30 "r": "reload",
31 "n": "notifications",
149bc23e
AS
32}
33
1882b5bc
AS
34_RC_PATHS = (
35 "~/.config/jan-pona-mute/login",
36 "~/.config/.jan-pona-mute",
37 "~/.jan-pona-mute"
38)
39
6328099f
AS
40_PAGERS = (
41 "mdcat",
42 "fold",
43 "cat"
44)
45
1882b5bc 46# Init file finder
149bc23e 47def get_rcfile():
1882b5bc 48 for rc_path in _RC_PATHS:
149bc23e
AS
49 rcfile = os.path.expanduser(rc_path)
50 if os.path.exists(rcfile):
51 return rcfile
52 return None
53
6328099f
AS
54# Pager finder
55def get_pager():
56 for cmd in _PAGERS:
57 pager = shutil.which(cmd)
58 if pager != None:
59 return pager
60
149bc23e
AS
61class DiasporaClient(cmd.Cmd):
62
63 prompt = "\x1b[38;5;255m" + "> " + "\x1b[0m"
6328099f 64 intro = "Welcome to Diaspora! Use the intro command for a quick introduction."
149bc23e 65
8bf63019
AS
66 header_format = "\x1b[1;38;5;255m" + "%s" + "\x1b[0m"
67
149bc23e
AS
68 username = None
69 pod = None
70 password = None
6328099f 71 pager = None
149bc23e
AS
72
73 connection = None
18e74209 74 notifications = []
1882b5bc 75 index = None
18e74209 76 post = None
bb8316e5 77 post_cache = {} # key is self.post.uid, and notification.id
f1bfb7bc 78
714a2f67
AS
79 undo = []
80
149bc23e
AS
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
6328099f 93 def do_intro(self, line):
d87d6adf 94 """Start here."""
6328099f 95 print("""
bb8316e5
AS
96Use the 'account' and 'password' commands to set up your connection,
97then use the 'login' command to log in. If everything works as
98intended, use the 'save' command to save these commands to an init
99file.
6328099f
AS
100
101Once you've listed things such as notifications, enter a number to
bb8316e5 102select the corresponding item.
6328099f
AS
103""")
104
149bc23e
AS
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('@')
1882b5bc 109 print("Username and pod set: %s@%s" % (self.username, self.pod))
149bc23e
AS
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:")
1882b5bc
AS
117 print("Username: %s" % self.username)
118 print("Password: %s" % ("None" if self.password == None else "set"))
119 print("Pod: %s" % self.pod)
6328099f 120 print("Pager: %s" % self.pager)
149bc23e
AS
121
122 def do_password(self, password):
123 """Set the password."""
124 self.password = (None if self.password == "" else password)
1882b5bc
AS
125 print("Password %s" % ("unset" if self.password == "" else "set"))
126
127 def do_save(self, line):
d87d6adf 128 """Save your login information to the init file."""
1882b5bc 129 if self.username == None or self.pod == None:
bb8316e5 130 print("Use the 'account' command to set username and pod")
1882b5bc 131 elif self.password == None:
bb8316e5 132 print("Use the 'password' command")
1882b5bc
AS
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)
149bc23e
AS
185
186 def do_login(self, line):
187 """Login."""
188 if line != "":
1882b5bc
AS
189 self.onecmd("account %s" % line)
190 if self.username == None or self.pod == None:
bb8316e5 191 print("Use the 'account' command to set username and pod")
1882b5bc 192 elif self.password == None:
bb8316e5 193 print("Use the 'password' command")
1882b5bc
AS
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")
149bc23e 202
6328099f
AS
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
8bf63019
AS
208 def header(self, line):
209 """Wrap line in header format."""
210 return self.header_format % line
211
149bc23e 212 def do_notifications(self, line):
bb8316e5
AS
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. 😢")
149bc23e
AS
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
bb8316e5
AS
246 # Finally, see if it's a notification and show it
247 self.do_show(line)
1882b5bc 248
bb8316e5 249 def do_show(self, line):
18e74209
AS
250 """Show the post given by the index number.
251The index number must refer to the current list of notifications."""
bb8316e5
AS
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
1882b5bc 258 try:
bb8316e5 259 n = int(line.strip())
18e74209
AS
260 notification = self.notifications[n-1]
261 self.index = n
bb8316e5
AS
262 except ValueError:
263 print("The 'show' command takes a notification number but '%s' is not a number" % line)
264 return
1882b5bc 265 except IndexError:
6328099f 266 print("Index too high!")
1882b5bc
AS
267 return
268
18e74209 269 self.show(notification)
bb8316e5 270 self.load(notification.about())
18e74209
AS
271
272 print()
273 self.show(self.post)
1882b5bc 274
bb8316e5
AS
275 if(self.post.comments):
276 print()
88c1a828
AS
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))
bb8316e5
AS
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),
285or 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
1882b5bc
AS
304 def show(self, item):
305 """Show the current item."""
6328099f 306 if self.pager:
88c1a828 307 subprocess.run(self.pager, input = str(item), text = True)
6328099f 308 else:
88c1a828 309 print(str(item))
6328099f 310
18e74209 311 def do_comments(self, line):
bb8316e5
AS
312 """Show the comments for the current post.
313Use the 'all' argument to show them all. Use a numerical argument to
314show that many. The default is to load the last five."""
18e74209 315 if self.post == None:
bb8316e5 316 print("Use the 'show' command to show a post, first.")
18e74209
AS
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 != "":
6328099f
AS
328 try:
329 n = int(line.strip())
330 except ValueError:
bb8316e5 331 print("The 'comments' command takes a number as its argument, or 'all'")
18e74209 332 print("The default is to show the last 5 comments")
6328099f 333 return
6328099f 334
18e74209
AS
335 if n != None:
336 comments = comments[-n:]
337
42716c49
AS
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.")
1882b5bc 346
714a2f67
AS
347 def do_comment(self, line):
348 """Leave a comment on the current post."""
349 if self.post == None:
bb8316e5 350 print("Use the 'show' command to show a post, first.")
714a2f67
AS
351 return
352 comment = self.post.comment(line)
353 self.post.comments.add(comment)
bb8316e5 354 self.undo.append("delete comment %s from %s" % (comment.id, self.id))
714a2f67
AS
355 print("Comment posted.")
356
f1bfb7bc
AS
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:
bb8316e5 363 print("Use the 'show' command to show a post, first.")
f1bfb7bc
AS
364 return
365 if len(words) != 4:
bb8316e5
AS
366 print("Deleting a comment requires a comment id and a post id.")
367 print("delete comment <comment id> from <post id>")
f1bfb7bc
AS
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
149bc23e
AS
387# Main function
388def main():
389
390 # Parse args
391 parser = argparse.ArgumentParser(description='A command line Diaspora client.')
1882b5bc
AS
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')
149bc23e
AS
394 args = parser.parse_args()
395
396 # Instantiate client
1882b5bc
AS
397 c = DiasporaClient()
398
399 # Process init file
6328099f 400 seen_pager = False
1882b5bc
AS
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()
6328099f
AS
408 if line != "":
409 c.cmdqueue.append(line)
410 if not seen_pager:
411 seen_pager = line.startswith("pager ");
1882b5bc 412 else:
bb8316e5 413 print("Use the 'save' command to save your login sequence to an init file")
149bc23e 414
6328099f
AS
415 if not seen_pager:
416 # prepend
417 c.cmdqueue.insert(0, "pager %s" % get_pager())
418
149bc23e
AS
419 # Endless interpret loop
420 while True:
421 try:
422 c.cmdloop()
423 except KeyboardInterrupt:
424 print("")
425
426if __name__ == '__main__':
427 main()