Commit | Line | Data |
---|---|---|
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 | ||
17 | import diaspy | |
6328099f | 18 | import subprocess |
149bc23e | 19 | import argparse |
6328099f | 20 | import shutil |
149bc23e AS |
21 | import cmd |
22 | import sys | |
23 | import 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 | 45 | def 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 |
53 | def get_pager(): | |
54 | for cmd in _PAGERS: | |
55 | pager = shutil.which(cmd) | |
56 | if pager != None: | |
57 | return pager | |
58 | ||
149bc23e AS |
59 | class 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(""" |
94 | Use the account and password commands to set up your connection, then | |
95 | use the login command to log in. If everything works as intended, use | |
96 | the save command to save these commands to an init file. | |
97 | ||
98 | Once you've listed things such as notifications, enter a number to | |
99 | select 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. | |
245 | The 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 |
341 | def 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 | ||
379 | if __name__ == '__main__': | |
380 | main() |