1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2004 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
12 #include <sys/types.h>
19 #include "liblj/consolecommand.h"
20 #include "liblj/login.h"
25 #include "journalstore.h"
27 #include "checkfriends.h"
32 #include "cmdline_data.h"
35 typedef struct _Command Command
;
40 char *username
, *password
, *postas
;
52 gboolean requireuser
, existinguser
, needpw
;
53 void (*action
) (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]);
57 static void command_dispatch (Cmdline
*cmdline
, Command
*commands
, const char *help
, int argc
, gchar
*argv
[]);
60 static void print_header (void) {
61 g_print(_("LogJam %s\nCopyright (C) 2000-2004 Evan Martin\n\n"), PACKAGE_VERSION
);
65 static void list_commands (Command
*commands
) {
69 for (c
= commands
; c
->cmdname
; ++c
) {
70 len
= strlen(c
->cmdname
);
74 fmt
= g_strdup_printf(" %%-%ds %%s\n", maxlen
);
75 for (c
= commands
; c
->cmdname
; ++c
) g_print(fmt
, _(c
->cmdname
), _(c
->desc
));
80 static void help_commands (Command
*commands
) {
81 g_print(_("Subcommands:\n"));
82 list_commands(commands
);
86 static void print_help (char *argv0
, Command
*commands
) {
88 g_print(_("Use: %s [options] [command [commandargs]]\n"), argv0
);
91 g_print(_("If no subcommand is provided, the default behavior is to load the GUI.\n"));
94 g_print(_(LOGJAM_HELP_TEXT
));
95 help_commands(commands
);
98 g_print(_("Also, GTK+ command line options (such as --display) can be used.\n"));
103 static void print_command_help(Command
*cmd
, const char *helptext
, Command
*subcmds
) {
104 g_print("%s -- %s", cmd
->cmdname
, helptext
);
106 help_commands(subcmds
);
110 #define OPT_LIST_SUBCOMMANDS (1)
113 #define _GETOPT_LOOP(name) { \
114 const char *short_options = name ## _SHORT_OPTIONS; \
115 struct option long_options[] = name ## _LONG_OPTIONS; \
118 int c = getopt_long(argc, argv, short_options, long_options, NULL); \
119 if (c == -1) break; \
121 case '?': /* unknown option character. */ \
122 case ':': /* missing option character. */ \
123 g_printerr(_("Unknown or missing option character\n")); \
127 #define GETOPT_LOOP_END \
133 #define _GETOPT_LOOP_SUBCOMMANDS(name) \
134 Command commands[] = name ## _SUBCOMMANDS; \
136 case OPT_LIST_SUBCOMMANDS: \
137 list_commands(commands); \
141 #define GETOPT_LOOP_SUBCOMMANDS_END(name) \
143 command_dispatch(cmdline, commands, name ## _HELP_TEXT, argc, argv);
146 #define GETOPT_LOOP(name) \
149 print_command_help(cmdline->curcmd, _(name ## _HELP_TEXT), NULL);
152 #define GETOPT_LOOP_SUBCOMMANDS(name) \
153 _GETOPT_LOOP_SUBCOMMANDS(name) \
155 print_command_help(cmdline->curcmd, _(name ## _HELP_TEXT), commands);
158 static void do_console (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
159 GString
*command
= g_string_new(NULL
);
160 LJConsoleCommand
*cc
;
164 GETOPT_LOOP_END
if (!JAM_ACCOUNT_IS_LJ(acc
)) {
165 g_printerr(_("Blogger accounts do not have a console interface.\n"));
169 ++argv
; /* skip 'console' */
171 g_printerr(_("Must specify a console command. Try \"help\".\n"));
175 if (argc
) g_string_append_printf(command
, "%s ", argv
++[0]); else g_string_append(command
, argv
++[0]);
177 cc
= lj_consolecommand_new(jam_account_lj_get_user(JAM_ACCOUNT_LJ(acc
)), command
->str
);
178 g_string_free(command
, TRUE
);
179 if (!net_run_verb_ctx((LJVerb
*) cc
, network_ctx_cmdline
, &err
)) {
180 g_print("Error: %s\n", err
->message
);
184 for (i
= 0; i
< cc
->linecount
; ++i
) {
185 g_print("%s\n", cc
->lines
[i
].text
);
187 lj_consolecommand_free(cc
);
192 static void do_post (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
193 /*gboolean use_editor = FALSE; */
195 case 'e': break; /*use_editor = TRUE; */
197 if (!jam_host_do_post(jam_account_get_host(acc
), network_ctx_cmdline
, cmdline
->doc
, NULL
)) {
200 if (!app
.quiet
) g_print(_("Success.\n"));
205 static void do_checkfriends (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
207 char *subcommand
= (argc
> 1 ? argv
[1] : NULL
);
209 GETOPT_LOOP(CHECKFRIENDS
)
211 if (!JAM_ACCOUNT_IS_LJ(acc
)) {
212 g_printerr(_("Checkfriends is only supported by LiveJournal accounts.\n"));
215 acclj
= JAM_ACCOUNT_LJ(acc
);
217 if (checkfriends_cli(acclj
)) {
219 exit(EXIT_SUCCESS
); /* NEW */
222 exit(EXIT_FAILURE
); /* no new entries, or some error */
224 } else if (!g_ascii_strcasecmp("purge", subcommand
)) {
225 char *id
= jam_account_id_strdup(acc
);
226 checkfriends_cli_purge(acclj
);
227 g_print(_("Checkfriends information for %s purged.\n"), id
);
231 g_printerr(_("Unknown argument %s to --checkfriends\n"), subcommand
);
237 static void do_offline_sync (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
238 GETOPT_LOOP(OFFLINE_SYNC
)
241 if (JAM_ACCOUNT_IS_LJ(acc
)) {
242 if (sync_run(JAM_ACCOUNT_LJ(acc
), NULL
)) exit(EXIT_SUCCESS
);
244 g_printerr(_("Sync is only supported by LiveJournal accounts.\n"));
250 static void do_offline_cat (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
251 LJEntryFileType output_type
= LJ_ENTRY_FILE_RFC822
;
252 JournalStore
*js
= NULL
;
255 gint counter
= 0; /* how many entries written. for rfc822 separator. */
256 gboolean mailbox
= FALSE
;
257 GETOPT_LOOP(OFFLINE_CAT
)
258 case 'm': mailbox
= TRUE
; break;
259 case 't': output_type
= LJ_ENTRY_FILE_RFC822
; break;
260 case 'x': output_type
= LJ_ENTRY_FILE_XML
; break;
262 if (optind
>= argc
) {
263 fprintf(stderr
, _("Specify at least one itemid or 'latest'.\n"));
266 if (!(js
= journal_store_open(acc
, FALSE
, &err
))) {
267 fprintf(stderr
, "%s", err
->message
);
271 while (optind
< argc
) {
273 gint num
= atoi(argv
[optind
]);
275 g_snprintf(snum
, 16, "%d", num
);
276 if (strncmp(snum
, argv
[optind
], 16)) {
277 if (strncmp(argv
[optind
], "latest", 7)) {
278 fprintf(stderr
, _("expected itemid, got %s\n"), argv
[optind
]);
281 num
= journal_store_get_latest_id(js
);
284 if (!(entry
= journal_store_get_entry(js
, num
))) {
285 fprintf(stderr
, _("entry %d not found\n"), num
);
288 switch (output_type
) {
289 case LJ_ENTRY_FILE_RFC822
:
291 /* entry separator lead-in: *two* newlines unlike RFC822
292 * email, so that later on in reading a entrybox we can
293 * tell an entry doesn't end with a newline. */
296 text
= lj_entry_to_rfc822(entry
, FALSE
);
298 time_t now
= time(NULL
);
299 g_print("From %s@%s %s"
300 "X-LogJam-Itemid: %d\n" "%s", jam_account_get_username(acc
), jam_account_get_host(acc
)->name
, ctime(&now
), num
, text
->str
);
302 g_print("X-LogJam-Itemid: %d\n"
303 "X-LogJam-User: %s\n"
304 "X-LogJam-Server: %s\n" "%s", num
, jam_account_get_username(acc
), jam_account_get_host(acc
)->name
, text
->str
);
306 g_string_free(text
, TRUE
);
308 case LJ_ENTRY_FILE_XML
:
310 fprintf(stderr
, "(xml cat not yet implemented.)\n");
313 lj_entry_free(entry
);
320 /* from regexp documentation in glibc */
321 static char *get_regerror (int errcode
, regex_t
*compiled
) {
322 size_t length
= regerror(errcode
, compiled
, NULL
, 0);
323 char *buffer
= g_new(char, length
);
324 regerror(errcode
, compiled
, buffer
, length
);
329 static void do_offline_grep (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
330 LJEntryFileType output_type
= LJ_ENTRY_FILE_RFC822
;
331 JournalStore
*js
= NULL
;
334 gint counter
= 0; /* how many entries written. for rfc822 separator. */
339 GETOPT_LOOP(OFFLINE_GREP
)
340 case 't': output_type
= LJ_ENTRY_FILE_RFC822
; break;
341 case 'x': output_type
= LJ_ENTRY_FILE_XML
; break;
342 case 'e': cflags
|= REG_EXTENDED
; break;
343 case 'i': cflags
|= REG_ICASE
; break;
345 if (optind
>= argc
) {
346 g_printerr(_("Specify a regexp to match\n"));
349 if (!(js
= journal_store_open(acc
, FALSE
, &err
))) {
350 g_printerr("%s", err
->message
);
354 if ((res
= regcomp(®exp
, argv
[optind
], cflags
|REG_NOSUB
))) {
355 gchar
*re_error
= get_regerror(res
, ®exp
);
356 g_printerr(_("Regexp error: %s.\n"), re_error
);
360 latest
= journal_store_get_latest_id(js
);
361 for (itemid
= 1; itemid
<= latest
; itemid
++) {
363 if (!(entry
= journal_store_get_entry(js
, itemid
))) continue; /* deleted item */
365 switch (regexec(®exp
, entry
->event
, 0, NULL
, 0)) {
366 case REG_NOMATCH
: continue; /* item does not match */
367 case REG_ESPACE
: g_printerr(_("Regexp error: out of memory.\n")); exit(EXIT_FAILURE
);
368 default: ; /* proceed */
370 switch (output_type
) {
371 case LJ_ENTRY_FILE_RFC822
:
373 /* entry separator lead-in: *two* newlines unlike RFC822
374 * email, so that later on in reading a entrybox we can
375 * tell an entry doesn't end with a newline. */
378 text
= lj_entry_to_rfc822(entry
, FALSE
);
379 g_print("X-LogJam-Itemid: %d\n"
380 "X-LogJam-User: %s\n"
381 "X-LogJam-Server: %s\n" "%s", itemid
, jam_account_get_username(acc
), jam_account_get_host(acc
)->name
, text
->str
);
382 g_string_free(text
, TRUE
);
384 case LJ_ENTRY_FILE_XML
:
386 g_printerr("Not yet implemented.\n");
389 lj_entry_free(entry
);
395 static void do_offline_summary (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
397 JournalStore
*js
= NULL
;
399 GETOPT_LOOP(OFFLINE_SUMMARY
)
401 command_dispatch(cmdline
, NULL
, _(OFFLINE_SUMMARY_HELP_TEXT
), argc
, argv
);
402 if (!(js
= journal_store_open(acc
, FALSE
, &err
))) {
403 fprintf(stderr
, "%s", err
->message
);
407 id
= jam_account_id_strdup(acc
);
408 g_print(_("Offline journal for '%s':\n"), id
);
410 lastsync
= journal_store_get_lastsync(js
);
411 g_print(_(" Last synced: %s.\n"), lastsync
);
413 g_print(_(" Entry count: %d.\n"), journal_store_get_count(js
));
414 g_print(_(" Latest itemid: %d.\n"), journal_store_get_latest_id(js
));
419 static void do_offline_reindex (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
421 GETOPT_LOOP(OFFLINE_REINDEX
)
423 command_dispatch(cmdline
, NULL
, _(OFFLINE_REINDEX_HELP_TEXT
), argc
, argv
);
424 g_print(_("Rebuilding index..."));
426 if (!journal_store_reindex(acc
, &err
)) {
427 g_print(_(" ERROR: %s.\n"), err
->message
);
431 g_print(_("done.\n"));
437 static void do_offline (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
438 GETOPT_LOOP_SUBCOMMANDS(OFFLINE
)
439 GETOPT_LOOP_SUBCOMMANDS_END(OFFLINE
)
440 print_command_help(cmdline
->curcmd
, _(OFFLINE_HELP_TEXT
), commands
);
444 static void do_user_add (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
445 const char *username
, *password
= NULL
;
446 gboolean remember_password
= TRUE
;
448 GETOPT_LOOP(USER_ADD
)
449 case 'p': remember_password
= FALSE
; break;
454 g_print(_("Error: specify a user.\n"));
455 g_print(_(USER_ADD_HELP_TEXT
));
459 if (remember_password
) {
461 password
= getpass("Password: ");
466 conf_verify_a_host_exists();
468 host
= conf
.lasthost
;
470 host
= conf
.hosts
->data
;
471 conf
.lasthost
= host
;
473 acc
= jam_host_get_account_by_username(host
, username
, /*create = */ TRUE
);
474 if (password
) jam_account_set_password(acc
, password
);
475 jam_account_set_remember(acc
, TRUE
, remember_password
);
476 conf_write(&conf
, app
.conf_dir
);
481 static void do_user (Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
482 GETOPT_LOOP_SUBCOMMANDS(USER
)
483 GETOPT_LOOP_SUBCOMMANDS_END(USER
)
484 print_command_help(cmdline
->curcmd
, _(USER_HELP_TEXT
), commands
);
488 JamAccount
*cmdline_load_account (Cmdline
*cmdline
, gboolean existinguser
, gboolean needpw
) {
491 if (!cmdline
->username
&& conf
.lasthost
&& conf
.lasthost
->lastaccount
&& conf
.lasthost
->lastaccount
->remember_password
) {
492 acc
= conf
.lasthost
->lastaccount
;
494 if (!cmdline
->username
) return NULL
;
495 if (conf
.lasthost
) host
= conf
.lasthost
;
496 else if (conf
.hosts
) host
= conf
.hosts
->data
;
498 acc
= jam_host_get_account_by_username(host
, cmdline
->username
, !existinguser
);
501 g_printerr(_("Unknown account '%s'.\n"), cmdline
->username
);
504 if (cmdline
->password
) jam_account_set_password(acc
, cmdline
->password
);
505 if (!jam_account_get_password(acc
) && needpw
) {
507 if ((password
= getpass(_("Password: "))) != NULL
) jam_account_set_password(acc
, password
);
510 // if (cmdline->postas)
511 // string_replace(&conf.usejournal, g_strdup(cmdline->postas));
512 /* if they're running a console command, we exit before we
513 * get a chance to write this new conf out. so we do it here. */
514 conf_write(&conf
, app
.conf_dir
);
519 gboolean
cmdline_load_file (JamDoc
*doc
, char *filename
, GError
**err
) {
520 if (strcmp(filename
, "-") == 0) {
522 entry
= lj_entry_new_from_file(stdin
, LJ_ENTRY_FILE_AUTODETECT
, NULL
, err
);
523 if (!entry
) return FALSE
;
524 jam_doc_load_entry(doc
, entry
);
525 lj_entry_free(entry
);
528 return jam_doc_load_file(doc
, filename
, LJ_ENTRY_FILE_AUTODETECT
, err
);
533 static void command_dispatch (Cmdline
*cmdline
, Command
*commands
, const char *help
, int argc
, gchar
*argv
[]) {
534 JamAccount
*acc
= NULL
;
535 Command
*command
= NULL
;
540 if (argc
<= 0) return;
541 if (g_ascii_strcasecmp(cmdname
, "help") == 0) {
545 for (int i
= 0; commands
&& commands
[i
].cmdname
; ++i
) {
546 if (g_ascii_strcasecmp(cmdname
, commands
[i
].cmdname
) == 0) {
547 command
= &commands
[i
];
552 g_printerr(_("Error: Unknown action '%s'.\n"), cmdname
);
555 cmdline
->curcmd
= command
;
556 if (command
->requireuser
) {
557 acc
= cmdline_load_account(cmdline
, command
->existinguser
, command
->needpw
);
559 g_printerr(_("Error: Must specify account.\n"));
562 jam_doc_set_account(cmdline
->doc
, acc
);
564 command
->action(cmdline
, acc
, argc
, argv
);
565 /* should terminate. */
566 g_error("not reached");
570 void cmdline_parse (JamDoc
*doc
, int argc
, char *argv
[]) {
571 Cmdline cmdline_real
= {.doc
= doc
};
572 Cmdline
*cmdline
= &cmdline_real
;
573 _GETOPT_LOOP_SUBCOMMANDS(LOGJAM
)
574 case 'h': print_help(argv
[0], commands
); exit(EXIT_SUCCESS
);
575 case 'v': print_header(); exit(EXIT_SUCCESS
);
576 case 'u': cmdline
->username
= optarg
; break;
577 case 'a': cmdline
->postas
= optarg
; break;
578 case 'p': cmdline
->password
= optarg
; break;
579 case 'e': cmdline
->use_editor
= TRUE
; break;
580 case 'q': app
.quiet
= TRUE
; break;
581 case 'f': cmdline_load_file(doc
, optarg
, NULL
); break; /* XXX error */
582 GETOPT_LOOP_SUBCOMMANDS_END(LOGJAM
)
583 /* if we get here, there wasn't a command to run. */
584 if (cmdline
->username
) {
586 acc
= cmdline_load_account(cmdline
, FALSE
, TRUE
);
587 jam_doc_set_account(doc
, acc
);
589 if (cmdline
->use_editor
) {
591 LJEntry
*entry
= jam_doc_get_entry(doc
);
592 if (!lj_entry_edit_with_usereditor(entry
, app
.conf_dir
, &err
)) {
593 g_printerr("%s", err
->message
);
596 jam_doc_load_entry(doc
, entry
);
597 lj_entry_free(entry
);