console.py - toot - Unnamed repository; edit this file 'description' to name the repository.
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) LICENSE
---
console.py (9208B)
---
1 # -*- coding: utf-8 -*-
2
3 import os
4 import sys
5 import logging
6
7 from argparse import ArgumentParser, FileType
8 from collections import namedtuple
9 from toot import config, commands, CLIENT_NAME, CLIENT_WEBSITE
10 from toot.exceptions import ApiError, ConsoleError
11 from toot.output import print_out, print_err
12
13 VISIBILITY_CHOICES = ['public', 'unlisted', 'private', 'direct']
14
15
16 def visibility(value):
17 """Validates the visibilty parameter"""
18 if value not in VISIBILITY_CHOICES:
19 raise ValueError("Invalid visibility value")
20
21 return value
22
23
24 Command = namedtuple("Command", ["name", "description", "require_auth", "arguments"])
25
26
27 common_args = [
28 (["--no-color"], {
29 "help": "don't use ANSI colors in output",
30 "action": 'store_true',
31 "default": False,
32 }),
33 (["--debug"], {
34 "help": "show debug log in console",
35 "action": 'store_true',
36 "default": False,
37 })
38 ]
39
40 account_arg = (["account"], {
41 "help": "account name, e.g. 'Gargron@mastodon.social'",
42 })
43
44 instance_arg = (["-i", "--instance"], {
45 "type": str,
46 "help": 'mastodon instance to log into e.g. "mastodon.social"',
47 })
48
49 email_arg = (["-e", "--email"], {
50 "type": str,
51 "help": 'email address to log in with',
52 })
53
54
55 AUTH_COMMANDS = [
56 Command(
57 name="login",
58 description="Log in from the console, does NOT support two factor authentication",
59 arguments=[instance_arg, email_arg],
60 require_auth=False,
61 ),
62 Command(
63 name="login_browser",
64 description="Log in using your browser, supports regular and two factor authentication",
65 arguments=[instance_arg],
66 require_auth=False,
67 ),
68 Command(
69 name="activate",
70 description="Switch between logged in accounts.",
71 arguments=[account_arg],
72 require_auth=False,
73 ),
74 Command(
75 name="logout",
76 description="Log out, delete stored access keys",
77 arguments=[account_arg],
78 require_auth=False,
79 ),
80 Command(
81 name="auth",
82 description="Show logged in accounts and instances",
83 arguments=[],
84 require_auth=False,
85 ),
86 ]
87
88 READ_COMMANDS = [
89 Command(
90 name="whoami",
91 description="Display logged in user details",
92 arguments=[],
93 require_auth=True,
94 ),
95 Command(
96 name="whois",
97 description="Display account details",
98 arguments=[
99 (["account"], {
100 "help": "account name or numeric ID"
101 }),
102 ],
103 require_auth=True,
104 ),
105 Command(
106 name="instance",
107 description="Display instance details",
108 arguments=[
109 (["instance"], {
110 "help": "instance domain (e.g. 'mastodon.social') or blank to use current",
111 "nargs": "?",
112 }),
113
114 ],
115 require_auth=False,
116 ),
117 Command(
118 name="search",
119 description="Search for users or hashtags",
120 arguments=[
121 (["query"], {
122 "help": "the search query",
123 }),
124 (["-r", "--resolve"], {
125 "action": 'store_true',
126 "default": False,
127 "help": "Resolve non-local accounts",
128 }),
129 ],
130 require_auth=True,
131 ),
132 Command(
133 name="timeline",
134 description="Show recent items in your public timeline",
135 arguments=[
136 (["tag"], {
137 "help" : "Search for a tag",
138 "nargs" : "?",
139 }),
140 ],
141 require_auth=True,
142 ),
143 Command(
144 name="curses",
145 description="An experimental timeline app (doesn't work on Windows)",
146 arguments=[
147 (["-p", "--public"], {
148 "action": 'store_true',
149 "default": False,
150 "help": "Resolve non-local accounts",
151 }),
152 (["-i", "--instance"], {
153 "type": str,
154 "help": 'instance from which to read (for public timeline only)',
155 })
156 ],
157 require_auth=False,
158 ),
159 ]
160
161 POST_COMMANDS = [
162 Command(
163 name="post",
164 description="Post a status text to your timeline",
165 arguments=[
166 (["text"], {
167 "help": "The status text to post.",
168 "nargs": "?",
169 }),
170 (["-m", "--media"], {
171 "type": FileType('rb'),
172 "help": "path to the media file to attach"
173 }),
174 (["-v", "--visibility"], {
175 "type": visibility,
176 "default": "public",
177 "help": 'post visibility, one of: %s' % ", ".join(VISIBILITY_CHOICES),
178 })
179 ],
180 require_auth=True,
181 ),
182 Command(
183 name="upload",
184 description="Upload an image or video file",
185 arguments=[
186 (["file"], {
187 "help": "Path to the file to upload",
188 "type": FileType('rb')
189 })
190 ],
191 require_auth=True,
192 ),
193 ]
194
195 ACCOUNTS_COMMANDS = [
196 Command(
197 name="follow",
198 description="Follow an account",
199 arguments=[
200 account_arg,
201 ],
202 require_auth=True,
203 ),
204 Command(
205 name="unfollow",
206 description="Unfollow an account",
207 arguments=[
208 account_arg,
209 ],
210 require_auth=True,
211 ),
212 Command(
213 name="mute",
214 description="Mute an account",
215 arguments=[
216 account_arg,
217 ],
218 require_auth=True,
219 ),
220 Command(
221 name="unmute",
222 description="Unmute an account",
223 arguments=[
224 account_arg,
225 ],
226 require_auth=True,
227 ),
228 Command(
229 name="block",
230 description="Block an account",
231 arguments=[
232 account_arg,
233 ],
234 require_auth=True,
235 ),
236 Command(
237 name="unblock",
238 description="Unblock an account",
239 arguments=[
240 account_arg,
241 ],
242 require_auth=True,
243 ),
244 ]
245
246 COMMANDS = AUTH_COMMANDS + READ_COMMANDS + POST_COMMANDS + ACCOUNTS_COMMANDS
247
248
249 def print_usage():
250 max_name_len = max(len(command.name) for command in COMMANDS)
251
252 groups = [
253 ("Authentication", AUTH_COMMANDS),
254 ("Read", READ_COMMANDS),
255 ("Post", POST_COMMANDS),
256 ("Accounts", ACCOUNTS_COMMANDS),
257 ]
258
259 print_out("<green>{}</green>".format(CLIENT_NAME))
260
261 for name, cmds in groups:
262 print_out("")
263 print_out(name + ":")
264
265 for cmd in cmds:
266 cmd_name = cmd.name.ljust(max_name_len + 2)
267 print_out(" <yellow>toot {}</yellow> {}".format(cmd_name, cmd.description))
268
269 print_out("")
270 print_out("To get help for each command run:")
271 print_out(" <yellow>toot <command> --help</yellow>")
272 print_out("")
273 print_out("<green>{}</green>".format(CLIENT_WEBSITE))
274
275
276 def get_argument_parser(name, command):
277 parser = ArgumentParser(
278 prog='toot %s' % name,
279 description=command.description,
280 epilog=CLIENT_WEBSITE)
281
282 for args, kwargs in command.arguments + common_args:
283 parser.add_argument(*args, **kwargs)
284
285 # If the command requires auth, give an option to select account
286 if command.require_auth:
287 parser.add_argument("-u", "--using", help="the account to use, overrides active account")
288
289 return parser
290
291
292 def run_command(app, user, name, args):
293 command = next((c for c in COMMANDS if c.name == name), None)
294
295 if not command:
296 print_err("Unknown command '{}'\n".format(name))
297 print_usage()
298 return
299
300 parser = get_argument_parser(name, command)
301 parsed_args = parser.parse_args(args)
302
303 # Override the active account if 'using' option is given
304 if command.require_auth and parsed_args.using:
305 user, app = config.get_user_app(parsed_args.using)
306 if not user or not app:
307 raise ConsoleError("User '{}' not found".format(parsed_args.using))
308
309 if command.require_auth and (not user or not app):
310 print_err("This command requires that you are logged in.")
311 print_err("Please run `toot login` first.")
312 return
313
314 fn = commands.__dict__.get(name)
315
316 if not fn:
317 raise NotImplementedError("Command '{}' does not have an implementation.".format(name))
318
319 return fn(app, user, parsed_args)
320
321
322 def main():
323 # Enable debug logging if --debug is in args
324 if "--debug" in sys.argv:
325 filename = os.getenv("TOOT_LOG_FILE")
326 logging.basicConfig(level=logging.DEBUG, filename=filename)
327
328 # If something is piped in, append it to commandline arguments
329 if not sys.stdin.isatty():
330 stdin = sys.stdin.read()
331 if stdin:
332 sys.argv.append(stdin)
333
334 command_name = sys.argv[1] if len(sys.argv) > 1 else None
335 args = sys.argv[2:]
336
337 if not command_name:
338 return print_usage()
339
340 user, app = config.get_active_user_app()
341
342 try:
343 run_command(app, user, command_name, args)
344 except ConsoleError as e:
345 print_err(str(e))
346 sys.exit(1)
347 except ApiError as e:
348 print_err(str(e))
349 sys.exit(1)