1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2004 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
14 #include <sys/types.h>
24 #include "liblj/consolecommand.h"
25 #include "liblj/login.h"
30 #include "journalstore.h"
32 #include "checkfriends.h"
37 #include "cmdline_data.h"
39 gboolean
net_run_verb_ctx(LJVerb
*verb
, NetContext
*ctx
, GError
**err
);
42 typedef struct _Command Command
;
46 char *username
, *password
, *postas
;
55 const char *cmdname
; const char *desc
;
56 gboolean requireuser
, existinguser
, needpw
;
57 void (*action
)(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]);
60 static void command_dispatch(Cmdline
*cmdline
, Command
*commands
, const char *help
, int argc
, gchar
*argv
[]);
63 /* this is a lame function. it isn't available on windows. */
65 getpass(const gchar
* prompt
) {
68 g_print("%s", prompt
);
69 for (i
= 0; i
< 64; i
++) {
70 gint ch
= getchar(); /* FIXME: this echoes */
71 if (ch
== EOF
|| ch
== (gint
)'\n' || ch
== (gint
)'\r' /* win32 */)
78 #endif /* HAVE_UNISTD_H */
82 g_print(_("LogJam %s\nCopyright (C) 2000-2004 Evan Martin\n\n"), PACKAGE_VERSION
);
86 list_commands(Command
*commands
) {
91 for (c
= commands
; c
->cmdname
; c
++) {
92 len
= strlen(c
->cmdname
);
96 fmt
= g_strdup_printf(" %%-%ds %%s\n", maxlen
);
97 for (c
= commands
; c
->cmdname
; c
++)
98 g_print(fmt
, _(c
->cmdname
), _(c
->desc
));
103 help_commands(Command
*commands
) {
104 g_print(_("Subcommands:\n"));
105 list_commands(commands
);
109 print_help(char *argv0
, Command
*commands
) {
112 g_print(_("Use: %s [options] [command [commandargs]]\n"), argv0
);
116 g_print(_("If no subcommand is provided, the default behavior is to load the GUI.\n"));
120 g_print(_(LOGJAM_HELP_TEXT
));
122 help_commands(commands
);
126 g_print(_("Also, GTK+ command line options (such as --display) can be used.\n"));
131 print_command_help(Command
*cmd
, const char *helptext
, Command
*subcmds
) {
132 g_print("%s -- %s", cmd
->cmdname
, helptext
);
134 help_commands(subcmds
);
138 #define OPT_LIST_SUBCOMMANDS 1
140 #define _GETOPT_LOOP(name) \
142 const char *short_options = name ## _SHORT_OPTIONS; \
143 struct option long_options[] = name ## _LONG_OPTIONS; \
146 int c = getopt_long(argc, argv, short_options, long_options, NULL); \
147 if (c == -1) break; \
150 case '?': /* unknown option character. */ \
151 case ':': /* missing option character. */ \
152 g_printerr(_("Unknown or missing option character\n")); \
155 #define GETOPT_LOOP_END \
160 #define _GETOPT_LOOP_SUBCOMMANDS(name) \
161 Command commands[] = name ## _SUBCOMMANDS; \
163 case OPT_LIST_SUBCOMMANDS: \
164 list_commands(commands); \
167 #define GETOPT_LOOP_SUBCOMMANDS_END(name) \
169 command_dispatch(cmdline, commands, name ## _HELP_TEXT, argc, argv);
171 #define GETOPT_LOOP(name) \
174 print_command_help(cmdline->curcmd, \
175 _(name ## _HELP_TEXT), NULL);
177 #define GETOPT_LOOP_SUBCOMMANDS(name) \
178 _GETOPT_LOOP_SUBCOMMANDS(name) \
180 print_command_help(cmdline->curcmd, \
181 _(name ## _HELP_TEXT), commands);
185 do_console(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
186 GString
*command
= g_string_new(NULL
);
187 LJConsoleCommand
*cc
;
194 if (!JAM_ACCOUNT_IS_LJ(acc
)) {
195 g_printerr(_("Blogger accounts do not have a console interface.\n"));
199 argc
--; argv
++; /* skip 'console' */
201 g_printerr(_("Must specify a console command. Try \"help\".\n"));
206 g_string_append_printf(command
, "%s ", argv
++[0]);
208 g_string_append(command
, argv
++[0]);
210 cc
= lj_consolecommand_new(jam_account_lj_get_user(JAM_ACCOUNT_LJ(acc
)),
212 g_string_free(command
, TRUE
);
214 if (!net_run_verb_ctx((LJVerb
*)cc
, network_ctx_cmdline
, &err
)) {
215 g_print("Error: %s\n", err
->message
);
220 for (i
= 0; i
< cc
->linecount
; i
++) {
221 g_print("%s\n", cc
->lines
[i
].text
);
224 lj_consolecommand_free(cc
);
230 do_post(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
231 gboolean use_editor
= FALSE
;
239 if (!jam_host_do_post(jam_account_get_host(acc
),
240 network_ctx_cmdline
, cmdline
->doc
, NULL
)) {
244 g_print(_("Success.\n"));
249 do_checkfriends(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
251 char *subcommand
= argc
> 1 ? argv
[1] : NULL
;
255 GETOPT_LOOP(CHECKFRIENDS
)
258 if (!JAM_ACCOUNT_IS_LJ(acc
)) {
259 g_printerr(_("Checkfriends is only supported by LiveJournal accounts.\n"));
262 acclj
= JAM_ACCOUNT_LJ(acc
);
265 if (checkfriends_cli(acclj
)) {
266 g_print("1\n"); exit(EXIT_SUCCESS
); /* NEW */
268 g_print("0\n"); exit(EXIT_FAILURE
); /* no new entries, or some error */
270 } else if (!g_ascii_strcasecmp("purge", subcommand
)) {
271 char *id
= jam_account_id_strdup(acc
);
272 checkfriends_cli_purge(acclj
);
273 g_print(_("Checkfriends information for %s purged.\n"), id
);
277 g_printerr(_("Unknown argument %s to --checkfriends\n"), subcommand
);
283 do_offline_sync(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
284 GETOPT_LOOP(OFFLINE_SYNC
)
288 if (JAM_ACCOUNT_IS_LJ(acc
)) {
289 if (sync_run(JAM_ACCOUNT_LJ(acc
), NULL
))
292 g_printerr(_("Sync is only supported by LiveJournal accounts.\n"));
298 do_offline_cat(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
299 LJEntryFileType output_type
= LJ_ENTRY_FILE_RFC822
;
300 JournalStore
*js
= NULL
;
303 gint counter
= 0; /* how many entries written. for rfc822 separator. */
304 gboolean mailbox
= FALSE
;
306 GETOPT_LOOP(OFFLINE_CAT
)
311 output_type
= LJ_ENTRY_FILE_RFC822
;
314 output_type
= LJ_ENTRY_FILE_XML
;
318 if (optind
>= argc
) {
320 _("Specify at least one itemid or 'latest'.\n"));
323 if (!(js
= journal_store_open(acc
, FALSE
, &err
))) {
329 while (optind
< argc
) {
331 gint num
= atoi(argv
[optind
]);
334 g_snprintf(snum
, 16, "%d", num
);
335 if (strncmp(snum
, argv
[optind
], 16)) {
336 if (strncmp(argv
[optind
], "latest", 7)) {
338 _("expected itemid, got %s\n"), argv
[optind
]);
341 num
= journal_store_get_latest_id(js
);
344 if (!(entry
= journal_store_get_entry(js
, num
))) {
346 _("entry %d not found\n"), num
);
349 switch (output_type
) {
350 case LJ_ENTRY_FILE_RFC822
:
352 /* entry separator lead-in: *two* newlines unlike RFC822
353 * email, so that later on in reading a entrybox we can
354 * tell an entry doesn't end with a newline. */
356 text
= lj_entry_to_rfc822(entry
, FALSE
);
358 time_t now
= time(NULL
);
359 g_print("From %s@%s %s"
360 "X-LogJam-Itemid: %d\n"
362 jam_account_get_username(acc
),
363 jam_account_get_host(acc
)->name
,
367 g_print("X-LogJam-Itemid: %d\n"
368 "X-LogJam-User: %s\n"
369 "X-LogJam-Server: %s\n"
371 num
, jam_account_get_username(acc
),
372 jam_account_get_host(acc
)->name
, text
->str
);
374 g_string_free(text
, TRUE
);
376 case LJ_ENTRY_FILE_XML
:
378 fprintf(stderr
, "(xml cat not yet implemented.)\n");
381 lj_entry_free(entry
);
388 /* from regexp documentation in glibc */
389 static char *get_regerror (int errcode
, regex_t
*compiled
)
391 size_t length
= regerror (errcode
, compiled
, NULL
, 0);
392 char *buffer
= g_new (char, length
);
393 (void) regerror (errcode
, compiled
, buffer
, length
);
396 #endif /* HAVE_REGEX_H */
399 do_offline_grep(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
401 LJEntryFileType output_type
= LJ_ENTRY_FILE_RFC822
;
402 JournalStore
*js
= NULL
;
405 gint counter
= 0; /* how many entries written. for rfc822 separator. */
411 GETOPT_LOOP(OFFLINE_GREP
)
413 output_type
= LJ_ENTRY_FILE_RFC822
;
416 output_type
= LJ_ENTRY_FILE_XML
;
419 cflags
|= REG_EXTENDED
;
426 if (optind
>= argc
) {
427 g_printerr(_("Specify a regexp to match\n"));
430 if (!(js
= journal_store_open(acc
, FALSE
, &err
))) {
431 g_printerr("%s", err
->message
);
435 if ((res
= regcomp(®exp
, argv
[optind
], cflags
| REG_NOSUB
))) {
436 gchar
*re_error
= get_regerror(res
, ®exp
);
437 g_printerr(_("Regexp error: %s.\n"), re_error
);
442 latest
= journal_store_get_latest_id(js
);
443 for (itemid
= 1; itemid
<= latest
; itemid
++) {
446 if (!(entry
= journal_store_get_entry(js
, itemid
)))
447 continue; /* deleted item */
450 switch (regexec(®exp
, entry
->event
, 0, NULL
, 0)) {
452 continue; /* item does not match */
454 g_printerr(_("Regexp error: out of memory.\n"));
460 switch (output_type
) {
461 case LJ_ENTRY_FILE_RFC822
:
463 /* entry separator lead-in: *two* newlines unlike RFC822
464 * email, so that later on in reading a entrybox we can
465 * tell an entry doesn't end with a newline. */
467 text
= lj_entry_to_rfc822(entry
, FALSE
);
468 g_print("X-LogJam-Itemid: %d\n"
469 "X-LogJam-User: %s\n"
470 "X-LogJam-Server: %s\n"
472 itemid
, jam_account_get_username(acc
),
473 jam_account_get_host(acc
)->name
, text
->str
);
474 g_string_free(text
, TRUE
);
476 case LJ_ENTRY_FILE_XML
:
478 g_printerr("Not yet implemented.\n");
481 lj_entry_free(entry
);
484 #else /* HAVE_REGEX_H */
485 g_printerr(_("LogJam was compiled without regular expression support.\n"));
487 #endif /* HAVE_REGEX_H */
491 do_offline_summary(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
493 JournalStore
*js
= NULL
;
496 GETOPT_LOOP(OFFLINE_SUMMARY
)
498 command_dispatch(cmdline
, NULL
, _(OFFLINE_SUMMARY_HELP_TEXT
), argc
, argv
);
500 if (!(js
= journal_store_open(acc
, FALSE
, &err
))) {
507 id
= jam_account_id_strdup(acc
);
508 g_print(_("Offline journal for '%s':\n"), id
);
511 lastsync
= journal_store_get_lastsync(js
);
512 g_print(_(" Last synced: %s.\n"), lastsync
);
515 g_print(_(" Entry count: %d.\n"), journal_store_get_count(js
));
516 g_print(_(" Latest itemid: %d.\n"), journal_store_get_latest_id(js
));
521 do_offline_reindex(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
524 GETOPT_LOOP(OFFLINE_REINDEX
)
526 command_dispatch(cmdline
, NULL
, _(OFFLINE_REINDEX_HELP_TEXT
), argc
, argv
);
528 g_print(_("Rebuilding index..."));
530 if (!journal_store_reindex(acc
, &err
)) {
531 g_print(_(" ERROR: %s.\n"), err
->message
);
535 g_print(_("done.\n"));
541 do_offline(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
542 GETOPT_LOOP_SUBCOMMANDS(OFFLINE
)
543 GETOPT_LOOP_SUBCOMMANDS_END(OFFLINE
)
544 print_command_help(cmdline
->curcmd
, _(OFFLINE_HELP_TEXT
), commands
);
549 do_user_add(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
550 const char *username
, *password
= NULL
;
551 gboolean remember_password
= TRUE
;
554 GETOPT_LOOP(USER_ADD
)
556 remember_password
= FALSE
;
564 g_print(_("Error: specify a user.\n"));
565 g_print(_(USER_ADD_HELP_TEXT
));
570 if (remember_password
) {
572 password
= getpass("Password: ");
578 conf_verify_a_host_exists();
580 host
= conf
.lasthost
;
582 host
= conf
.hosts
->data
;
583 conf
.lasthost
= host
;
586 acc
= jam_host_get_account_by_username(host
, username
, /*create = */TRUE
);
588 jam_account_set_password(acc
, password
);
589 jam_account_set_remember(acc
, TRUE
, remember_password
);
590 conf_write(&conf
, app
.conf_dir
);
595 do_user(Cmdline
*cmdline
, JamAccount
*acc
, gint argc
, gchar
*argv
[]) {
596 GETOPT_LOOP_SUBCOMMANDS(USER
)
597 GETOPT_LOOP_SUBCOMMANDS_END(USER
)
598 print_command_help(cmdline
->curcmd
, _(USER_HELP_TEXT
), commands
);
602 cmdline_load_account(Cmdline
*cmdline
, gboolean existinguser
, gboolean needpw
) {
606 if (!cmdline
->username
&&
607 conf
.lasthost
&& conf
.lasthost
->lastaccount
&&
608 conf
.lasthost
->lastaccount
->remember_password
) {
609 acc
= conf
.lasthost
->lastaccount
;
611 if (!cmdline
->username
)
615 host
= conf
.lasthost
;
617 host
= conf
.hosts
->data
;
621 acc
= jam_host_get_account_by_username(host
, cmdline
->username
,
626 g_printerr(_("Unknown account '%s'.\n"), cmdline
->username
);
630 if (cmdline
->password
)
631 jam_account_set_password(acc
, cmdline
->password
);
632 if (!jam_account_get_password(acc
) && needpw
) {
634 if ((password
= getpass(_("Password: "))) != NULL
)
635 jam_account_set_password(acc
, password
);
639 // if (cmdline->postas)
640 // string_replace(&conf.usejournal, g_strdup(cmdline->postas));
642 /* if they're running a console command, we exit before we
643 * get a chance to write this new conf out. so we do it here. */
644 conf_write(&conf
, app
.conf_dir
);
650 cmdline_load_file(JamDoc
*doc
, char *filename
, GError
**err
) {
651 if (strcmp(filename
, "-") == 0) {
653 entry
= lj_entry_new_from_file(stdin
,
654 LJ_ENTRY_FILE_AUTODETECT
, NULL
, err
);
655 if (!entry
) return FALSE
;
656 jam_doc_load_entry(doc
, entry
);
657 lj_entry_free(entry
);
660 return jam_doc_load_file(doc
, filename
, LJ_ENTRY_FILE_AUTODETECT
, err
);
665 command_dispatch(Cmdline
*cmdline
, Command
*commands
, const char *help
, int argc
, gchar
*argv
[]) {
667 Command
*command
= NULL
;
678 if (g_ascii_strcasecmp(cmdname
, "help") == 0) {
682 for (i
= 0; commands
&& commands
[i
].cmdname
; i
++) {
683 if (g_ascii_strcasecmp(cmdname
, commands
[i
].cmdname
) == 0) {
684 command
= &commands
[i
];
689 g_printerr(_("Error: Unknown action '%s'.\n"), cmdname
);
692 cmdline
->curcmd
= command
;
693 if (command
->requireuser
) {
694 acc
= cmdline_load_account(cmdline
,
695 command
->existinguser
, command
->needpw
);
697 g_printerr(_("Error: Must specify account.\n"));
700 jam_doc_set_account(cmdline
->doc
, acc
);
702 command
->action(cmdline
, acc
, argc
, argv
);
703 /* should terminate. */
704 g_error("not reached");
708 cmdline_parse(JamDoc
*doc
, int argc
, char *argv
[]) {
709 Cmdline cmdline_real
= { .doc
= doc
};
710 Cmdline
*cmdline
= &cmdline_real
;
712 _GETOPT_LOOP_SUBCOMMANDS(LOGJAM
)
714 print_help(argv
[0], commands
);
720 cmdline
->username
= optarg
;
723 cmdline
->postas
= optarg
;
726 cmdline
->password
= optarg
;
729 cmdline
->use_editor
= TRUE
;
735 cmdline_load_file(doc
, optarg
, NULL
); /* XXX error */
737 GETOPT_LOOP_SUBCOMMANDS_END(LOGJAM
)
739 /* if we get here, there wasn't a command to run. */
740 if (cmdline
->username
) {
742 acc
= cmdline_load_account(cmdline
, FALSE
, TRUE
);
743 jam_doc_set_account(doc
, acc
);
745 if (cmdline
->use_editor
) {
747 LJEntry
*entry
= jam_doc_get_entry(doc
);
748 if (!lj_entry_edit_with_usereditor(entry
, app
.conf_dir
, &err
)) {
749 g_printerr("%s", err
->message
);
752 jam_doc_load_entry(doc
, entry
);
753 lj_entry_free(entry
);