README: add installation and quickstart
[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",
149bc23e
AS
30}
31
1882b5bc
AS
32_RC_PATHS = (
33 "~/.config/jan-pona-mute/login",
34 "~/.config/.jan-pona-mute",
35 "~/.jan-pona-mute"
36)
37
6328099f
AS
38_PAGERS = (
39 "mdcat",
40 "fold",
41 "cat"
42)
43
1882b5bc 44# Init file finder
149bc23e 45def get_rcfile():
1882b5bc 46 for rc_path in _RC_PATHS:
149bc23e
AS
47 rcfile = os.path.expanduser(rc_path)
48 if os.path.exists(rcfile):
49 return rcfile
50 return None
51
6328099f
AS
52# Pager finder
53def get_pager():
54 for cmd in _PAGERS:
55 pager = shutil.which(cmd)
56 if pager != None:
57 return pager
58
149bc23e
AS
59class DiasporaClient(cmd.Cmd):
60
61 prompt = "\x1b[38;5;255m" + "> " + "\x1b[0m"
6328099f 62 intro = "Welcome to Diaspora! Use the intro command for a quick introduction."
149bc23e 63
8bf63019
AS
64 header_format = "\x1b[1;38;5;255m" + "%s" + "\x1b[0m"
65
149bc23e
AS
66 username = None
67 pod = None
68 password = None
6328099f 69 pager = None
149bc23e
AS
70
71 connection = None
18e74209 72 notifications = []
1882b5bc 73 index = None
18e74209 74 post = None
f1bfb7bc
AS
75 post_cache = {} # key is self.post.uid
76
714a2f67
AS
77 undo = []
78
149bc23e
AS
79
80 # dict mapping user ids to usernames
81 users = {}
82
83 def get_username(self, guid):
84 if guid in self.users:
85 return self.users[guid]
86 else:
87 user = diaspy.people.User(connection = self.connection, guid = guid)
88 self.users[guid] = user.handle()
89 return self.users[guid]
90
6328099f 91 def do_intro(self, line):
d87d6adf 92 """Start here."""
6328099f
AS
93 print("""
94Use the account and password commands to set up your connection, then
95use the login command to log in. If everything works as intended, use
96the save command to save these commands to an init file.
97
98Once you've listed things such as notifications, enter a number to
99select the corresponding item. Use the print command to see more.
100""")
101
149bc23e
AS
102 def do_account(self, account):
103 """Set username and pod using the format username@pod."""
104 try:
105 (self.username, self.pod) = account.split('@')
1882b5bc 106 print("Username and pod set: %s@%s" % (self.username, self.pod))
149bc23e
AS
107 except ValueError:
108 print("The account must contain an @ character, e.g. kensanata@pluspora.com.")
109 print("Use the account comand to set the account.")
110
111 def do_info(self, line):
112 """Get some info about things. By default, it is info about yourself."""
113 print("Info about yourself:")
1882b5bc
AS
114 print("Username: %s" % self.username)
115 print("Password: %s" % ("None" if self.password == None else "set"))
116 print("Pod: %s" % self.pod)
6328099f 117 print("Pager: %s" % self.pager)
149bc23e
AS
118
119 def do_password(self, password):
120 """Set the password."""
121 self.password = (None if self.password == "" else password)
1882b5bc
AS
122 print("Password %s" % ("unset" if self.password == "" else "set"))
123
124 def do_save(self, line):
d87d6adf 125 """Save your login information to the init file."""
1882b5bc
AS
126 if self.username == None or self.pod == None:
127 print("Use the account command to set username and pod")
128 elif self.password == None:
129 print("Use the password command")
130 else:
131 rcfile = get_rcfile()
132 if rcfile == None:
133 rfile = first(_RC_PATHS)
134 seen_account = False
135 seen_password = False
136 seen_login = False
137 changed = False
138 file = []
139 with open(rcfile, "r") as fp:
140 for line in fp:
141 words = line.strip().split()
142 if words:
143 if words[0] == "account":
144 seen_account = True
145 account = "%s@%s" % (self.username, self.pod)
146 if len(words) > 1 and words[1] != account:
147 line = "account %s\n" % account
148 changed = True
149 elif words[0] == "password":
150 seen_password = True
151 if len(words) > 1 and words[1] != self.password:
152 line = "password %s\n" % self.password
153 changed = True
154 elif words[0] == "login":
155 if seen_account and seen_password:
156 seen_login = True
157 else:
158 # skip login if no account or no password given
159 line = None
160 changed = True
161 if line != None:
162 file.append(line)
163 if not seen_account:
164 file.append("account %s@%s\n" % (self.username, self.pod))
165 changed = True
166 if not seen_password:
167 file.append("password %s\n" % self.password)
168 changed = True
169 if not seen_login:
170 file.append("login\n")
171 changed = True
172 if changed:
173 if os.path.isfile(rcfile):
174 os.rename(rcfile, rcfile + "~")
175 if not os.path.isdir(os.path.dirname(rcfile)):
176 os.makedirs(os.path.dirname(rcfile))
177 with open(rcfile, "w") as fp:
178 fp.write("".join(file))
179 print("Wrote %s" % rcfile)
180 else:
181 print("No changes made, %s left unchanged" % rcfile)
149bc23e
AS
182
183 def do_login(self, line):
184 """Login."""
185 if line != "":
1882b5bc
AS
186 self.onecmd("account %s" % line)
187 if self.username == None or self.pod == None:
188 print("Use the account command to set username and pod")
189 elif self.password == None:
190 print("Use the password command")
191 else:
192 self.connection = diaspy.connection.Connection(
193 pod = "https://%s" % self.pod, username = self.username, password = self.password)
194 try:
195 self.connection.login()
196 self.onecmd("notifications")
197 except diaspy.errors.LoginError:
198 print("Login failed")
149bc23e 199
6328099f
AS
200 def do_pager(self, pager):
201 """Set the pager, e.g. to cat"""
202 self.pager = pager
203 print("Pager set: %s" % self.pager)
204
8bf63019
AS
205 def header(self, line):
206 """Wrap line in header format."""
207 return self.header_format % line
208
149bc23e 209 def do_notifications(self, line):
1882b5bc 210 """List notifications."""
149bc23e
AS
211 if self.connection == None:
212 print("Use the login command, first.")
213 return
18e74209
AS
214 self.notifications = diaspy.notifications.Notifications(self.connection).last()
215 for n, notification in enumerate(self.notifications):
8bf63019 216 print(self.header("%2d. %s %s") % (n+1, notification.when(), notification))
6328099f 217 print("Enter a number to select the notification.")
149bc23e
AS
218
219 ### The end!
220 def do_quit(self, *args):
221 """Exit jan-pona-mute."""
222 print("Be safe!")
223 sys.exit()
224
225 def default(self, line):
226 if line.strip() == "EOF":
227 return self.onecmd("quit")
228
229 # Expand abbreviated commands
230 first_word = line.split()[0].strip()
231 if first_word in _ABBREVS:
232 full_cmd = _ABBREVS[first_word]
233 expanded = line.replace(first_word, full_cmd, 1)
234 return self.onecmd(expanded)
235
1882b5bc
AS
236 try:
237 n = int(line.strip())
18e74209
AS
238 # Finally, see if it's a notification and show it
239 self.do_show(n)
1882b5bc 240 except ValueError:
18e74209 241 print("Use the help command to show available commands")
1882b5bc 242
18e74209
AS
243 def do_show(self, n):
244 """Show the post given by the index number.
245The index number must refer to the current list of notifications."""
1882b5bc 246 try:
18e74209
AS
247 notification = self.notifications[n-1]
248 self.index = n
1882b5bc 249 except IndexError:
6328099f 250 print("Index too high!")
1882b5bc
AS
251 return
252
18e74209
AS
253 self.show(notification)
254
255 print("Loading...")
256 self.post = diaspy.models.Post(connection = self.connection, id = notification.about())
f1bfb7bc 257 self.post_cache[self.post.guid] = self.post
18e74209
AS
258
259 print()
260 self.show(self.post)
1882b5bc
AS
261
262 def show(self, item):
263 """Show the current item."""
6328099f
AS
264 if self.pager:
265 subprocess.run(self.pager, input = repr(item), text = True)
266 else:
267 print(repr(item))
268
18e74209
AS
269 def do_comments(self, line):
270 """Show the comments for the current post."""
271 if self.post == None:
272 print("Use the show command to show a post, first.")
273 return
274 if self.post.comments == None:
275 print("The current post has no comments.")
276 return
277
278 n = 5
279 comments = self.post.comments
280
281 if line == "all":
282 n = None
283 elif line != "":
6328099f
AS
284 try:
285 n = int(line.strip())
286 except ValueError:
18e74209
AS
287 print("The comments command takes a number as its argument, or 'all'")
288 print("The default is to show the last 5 comments")
6328099f 289 return
6328099f 290
18e74209
AS
291 if n != None:
292 comments = comments[-n:]
293
294 for n, comment in enumerate(comments):
295 print()
8bf63019 296 print(self.header("%2d. %s %s") % (n+1, comment.when(), comment.author()))
18e74209
AS
297 print()
298 self.show(comment)
1882b5bc 299
714a2f67
AS
300 def do_comment(self, line):
301 """Leave a comment on the current post."""
302 if self.post == None:
303 print("Use the show command to show a post, first.")
304 return
305 comment = self.post.comment(line)
306 self.post.comments.add(comment)
307 self.undo.append("delete comment %s from %s" % (comment.id, self.post.guid))
308 print("Comment posted.")
309
f1bfb7bc
AS
310 def do_delete(self, line):
311 """Delete a comment."""
312 words = line.strip().split()
313 if words:
314 if words[0] == "comment":
315 if self.post == None:
316 print("Use the show command to show a post, first.")
317 return
318 if len(words) != 4:
319 print("Deleting a comment requires a comment id and a post guid.")
320 print("delete comment <id> from <guid>")
321 return
322 self.post_cache[words[3]].delete_comment(words[1])
323 print("Comment deleted.")
324 else:
325 print("Deleting '%s' is not supported." % words[0])
326 return
327 else:
328 print("Delete what?")
329
330 def do_undo(self, line):
331 """Undo an action."""
332 if line != "":
333 print("Undo does not take an argument.")
334 return
335 if not self.undo:
336 print("There is nothing to undo.")
337 return
338 return self.onecmd(self.undo.pop())
339
149bc23e
AS
340# Main function
341def main():
342
343 # Parse args
344 parser = argparse.ArgumentParser(description='A command line Diaspora client.')
1882b5bc
AS
345 parser.add_argument('--no-init-file', dest='init_file', action='store_const',
346 const=False, default=True, help='Do not load a init file')
149bc23e
AS
347 args = parser.parse_args()
348
349 # Instantiate client
1882b5bc
AS
350 c = DiasporaClient()
351
352 # Process init file
6328099f 353 seen_pager = False
1882b5bc
AS
354 if args.init_file:
355 rcfile = get_rcfile()
356 if rcfile:
357 print("Using init file %s" % rcfile)
358 with open(rcfile, "r") as fp:
359 for line in fp:
360 line = line.strip()
6328099f
AS
361 if line != "":
362 c.cmdqueue.append(line)
363 if not seen_pager:
364 seen_pager = line.startswith("pager ");
1882b5bc
AS
365 else:
366 print("Use the save command to save your login sequence to an init file")
149bc23e 367
6328099f
AS
368 if not seen_pager:
369 # prepend
370 c.cmdqueue.insert(0, "pager %s" % get_pager())
371
149bc23e
AS
372 # Endless interpret loop
373 while True:
374 try:
375 c.cmdloop()
376 except KeyboardInterrupt:
377 print("")
378
379if __name__ == '__main__':
380 main()