diff --git a/Makefile.am b/Makefile.am index 2a906a8e..76b16d57 100644 --- a/Makefile.am +++ b/Makefile.am @@ -20,6 +20,7 @@ core_sources = \ src/xmpp/form.c src/xmpp/form.h \ src/xmpp/avatar.c src/xmpp/avatar.h \ src/xmpp/ox.c src/xmpp/ox.h \ + src/xmpp/vcard.c src/xmpp/vcard.h src/xmpp/vcard_funcs.h \ src/event/common.c src/event/common.h \ src/event/server_events.c src/event/server_events.h \ src/event/client_events.c src/event/client_events.h \ @@ -37,6 +38,7 @@ core_sources = \ src/ui/privwin.c \ src/ui/confwin.c \ src/ui/xmlwin.c \ + src/ui/vcardwin.c \ src/command/cmd_defs.h src/command/cmd_defs.c \ src/command/cmd_funcs.h src/command/cmd_funcs.c \ src/command/cmd_ac.h src/command/cmd_ac.c \ @@ -88,6 +90,7 @@ unittest_sources = \ src/omemo/omemo.h \ src/omemo/crypto.h \ src/omemo/store.h \ + src/xmpp/vcard.h src/xmpp/vcard_funcs.h \ src/command/cmd_defs.h src/command/cmd_defs.c \ src/command/cmd_funcs.h src/command/cmd_funcs.c \ src/command/cmd_ac.h src/command/cmd_ac.c \ @@ -119,11 +122,13 @@ unittest_sources = \ src/event/server_events.c src/event/server_events.h \ src/event/client_events.c src/event/client_events.h \ src/ui/tray.h src/ui/tray.c \ + tests/unittests/xmpp/stub_vcard.c \ tests/unittests/xmpp/stub_avatar.c \ tests/unittests/xmpp/stub_ox.c \ tests/unittests/xmpp/stub_xmpp.c \ tests/unittests/xmpp/stub_message.c \ tests/unittests/ui/stub_ui.c tests/unittests/ui/stub_ui.h \ + tests/unittests/ui/stub_vcardwin.c \ tests/unittests/log/stub_log.c \ tests/unittests/chatlog/stub_chatlog.c \ tests/unittests/database/stub_database.c \ diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 93a503d1..9a839258 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -41,6 +41,7 @@ #include #include #include +#include #include "common.h" #include "config/preferences.h" @@ -131,6 +132,7 @@ static char* _lastactivity_autocomplete(ProfWin* window, const char* const input static char* _intype_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _mood_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previous); +static char* _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context); @@ -276,6 +278,15 @@ static Autocomplete mood_ac; static Autocomplete mood_type_ac; static Autocomplete adhoc_cmd_ac; static Autocomplete lastactivity_ac; +static Autocomplete vcard_ac; +static Autocomplete vcard_photo_ac; +static Autocomplete vcard_element_ac; +static Autocomplete vcard_set_ac; +static Autocomplete vcard_name_ac; +static Autocomplete vcard_set_param_ac; +static Autocomplete vcard_togglable_param_ac; +static Autocomplete vcard_toggle_ac; +static Autocomplete vcard_address_type_ac; /*! * \brief Initialization of auto completion for commands. @@ -1181,6 +1192,89 @@ cmd_ac_init(void) lastactivity_ac = autocomplete_new(); autocomplete_add(lastactivity_ac, "set"); autocomplete_add(lastactivity_ac, "get"); + + vcard_ac = autocomplete_new(); + autocomplete_add(vcard_ac, "get"); + autocomplete_add(vcard_ac, "photo"); + autocomplete_add(vcard_ac, "set"); + autocomplete_add(vcard_ac, "add"); + autocomplete_add(vcard_ac, "remove"); + autocomplete_add(vcard_ac, "save"); + + vcard_photo_ac = autocomplete_new(); + autocomplete_add(vcard_photo_ac, "open"); + autocomplete_add(vcard_photo_ac, "save"); + + vcard_element_ac = autocomplete_new(); + autocomplete_add(vcard_element_ac, "nickname"); + autocomplete_add(vcard_element_ac, "birthday"); + autocomplete_add(vcard_element_ac, "address"); + autocomplete_add(vcard_element_ac, "tel"); + autocomplete_add(vcard_element_ac, "email"); + autocomplete_add(vcard_element_ac, "jid"); + autocomplete_add(vcard_element_ac, "title"); + autocomplete_add(vcard_element_ac, "role"); + autocomplete_add(vcard_element_ac, "note"); + autocomplete_add(vcard_element_ac, "url"); + + vcard_set_ac = autocomplete_new(); + autocomplete_add(vcard_set_ac, "fullname"); + autocomplete_add(vcard_set_ac, "name"); + + vcard_name_ac = autocomplete_new(); + autocomplete_add(vcard_name_ac, "family"); + autocomplete_add(vcard_name_ac, "given"); + autocomplete_add(vcard_name_ac, "middle"); + autocomplete_add(vcard_name_ac, "prefix"); + autocomplete_add(vcard_name_ac, "suffix"); + + vcard_set_param_ac = autocomplete_new(); + autocomplete_add(vcard_set_param_ac, "pobox"); + autocomplete_add(vcard_set_param_ac, "extaddr"); + autocomplete_add(vcard_set_param_ac, "street"); + autocomplete_add(vcard_set_param_ac, "locality"); + autocomplete_add(vcard_set_param_ac, "region"); + autocomplete_add(vcard_set_param_ac, "pocode"); + autocomplete_add(vcard_set_param_ac, "country"); + autocomplete_add(vcard_set_param_ac, "type"); + autocomplete_add(vcard_set_param_ac, "home"); + autocomplete_add(vcard_set_param_ac, "work"); + autocomplete_add(vcard_set_param_ac, "voice"); + autocomplete_add(vcard_set_param_ac, "fax"); + autocomplete_add(vcard_set_param_ac, "pager"); + autocomplete_add(vcard_set_param_ac, "msg"); + autocomplete_add(vcard_set_param_ac, "cell"); + autocomplete_add(vcard_set_param_ac, "video"); + autocomplete_add(vcard_set_param_ac, "bbs"); + autocomplete_add(vcard_set_param_ac, "modem"); + autocomplete_add(vcard_set_param_ac, "isdn"); + autocomplete_add(vcard_set_param_ac, "pcs"); + autocomplete_add(vcard_set_param_ac, "preferred"); + autocomplete_add(vcard_set_param_ac, "x400"); + + vcard_togglable_param_ac = autocomplete_new(); + autocomplete_add(vcard_togglable_param_ac, "home"); + autocomplete_add(vcard_togglable_param_ac, "work"); + autocomplete_add(vcard_togglable_param_ac, "voice"); + autocomplete_add(vcard_togglable_param_ac, "fax"); + autocomplete_add(vcard_togglable_param_ac, "pager"); + autocomplete_add(vcard_togglable_param_ac, "msg"); + autocomplete_add(vcard_togglable_param_ac, "cell"); + autocomplete_add(vcard_togglable_param_ac, "video"); + autocomplete_add(vcard_togglable_param_ac, "bbs"); + autocomplete_add(vcard_togglable_param_ac, "modem"); + autocomplete_add(vcard_togglable_param_ac, "isdn"); + autocomplete_add(vcard_togglable_param_ac, "pcs"); + autocomplete_add(vcard_togglable_param_ac, "preferred"); + autocomplete_add(vcard_togglable_param_ac, "x400"); + + vcard_toggle_ac = autocomplete_new(); + autocomplete_add(vcard_toggle_ac, "on"); + autocomplete_add(vcard_toggle_ac, "off"); + + vcard_address_type_ac = autocomplete_new(); + autocomplete_add(vcard_address_type_ac, "domestic"); + autocomplete_add(vcard_address_type_ac, "international"); } void @@ -1502,6 +1596,17 @@ cmd_ac_reset(ProfWin* window) autocomplete_reset(mood_ac); autocomplete_reset(mood_type_ac); autocomplete_reset(adhoc_cmd_ac); + + autocomplete_reset(vcard_ac); + autocomplete_reset(vcard_photo_ac); + autocomplete_reset(vcard_element_ac); + autocomplete_reset(vcard_set_ac); + autocomplete_reset(vcard_name_ac); + autocomplete_reset(vcard_set_param_ac); + autocomplete_reset(vcard_togglable_param_ac); + autocomplete_reset(vcard_toggle_ac); + autocomplete_reset(vcard_address_type_ac); + autocomplete_reset(script_ac); autocomplete_reset(lastactivity_ac); @@ -1672,6 +1777,15 @@ cmd_ac_uninit(void) autocomplete_free(intype_ac); autocomplete_free(adhoc_cmd_ac); autocomplete_free(lastactivity_ac); + autocomplete_free(vcard_ac); + autocomplete_free(vcard_photo_ac); + autocomplete_free(vcard_element_ac); + autocomplete_free(vcard_set_ac); + autocomplete_free(vcard_name_ac); + autocomplete_free(vcard_set_param_ac); + autocomplete_free(vcard_togglable_param_ac); + autocomplete_free(vcard_toggle_ac); + autocomplete_free(vcard_address_type_ac); } static void @@ -1944,6 +2058,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ g_hash_table_insert(ac_funcs, "/intype", _intype_autocomplete); g_hash_table_insert(ac_funcs, "/mood", _mood_autocomplete); g_hash_table_insert(ac_funcs, "/cmd", _adhoc_cmd_autocomplete); + g_hash_table_insert(ac_funcs, "/vcard", _vcard_autocomplete); int len = strlen(input); char parsed[len + 1]; @@ -4322,3 +4437,138 @@ _adhoc_cmd_autocomplete(ProfWin* window, const char* const input, gboolean previ return result; } + +static char* +_vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + char* result = NULL; + + gboolean parse_result = FALSE; + gchar** args = parse_args(input, 0, 7, &parse_result); + + if (parse_result && (g_strcmp0(args[0], "set") == 0)) { + gboolean space_at_end = g_str_has_suffix(input, " "); + int num_args = g_strv_length(args); + gboolean is_num = TRUE; + + if (num_args >= 2) { + for (int i = 0; i < strlen(args[1]); i++) { + if (!isdigit((int)args[1][i])) { + is_num = FALSE; + break; + } + } + } + + if ((num_args == 2 && space_at_end && is_num) || (num_args == 3 && !space_at_end && is_num)) { + GString* beginning = g_string_new("/vcard"); + g_string_append_printf(beginning, " %s %s", args[0], args[1]); + result = autocomplete_param_with_ac(input, beginning->str, vcard_set_param_ac, TRUE, previous); + g_string_free(beginning, TRUE); + if (result) { + g_strfreev(args); + return result; + } + } else if ((num_args == 3 && space_at_end && is_num && (g_strcmp0(args[2], "type") == 0)) || (num_args == 4 && !space_at_end && is_num && (g_strcmp0(args[2], "type") == 0))) { + GString* beginning = g_string_new("/vcard"); + g_string_append_printf(beginning, " %s %s %s", args[0], args[1], args[2]); + result = autocomplete_param_with_ac(input, beginning->str, vcard_address_type_ac, TRUE, previous); + g_string_free(beginning, TRUE); + if (result) { + g_strfreev(args); + return result; + } + } else if ((num_args == 3 && space_at_end && is_num && autocomplete_contains(vcard_togglable_param_ac, args[2])) || (num_args == 4 && !space_at_end && is_num && autocomplete_contains(vcard_togglable_param_ac, args[2]))) { + GString* beginning = g_string_new("/vcard"); + g_string_append_printf(beginning, " %s %s %s", args[0], args[1], args[2]); + result = autocomplete_param_with_ac(input, beginning->str, vcard_toggle_ac, TRUE, previous); + g_string_free(beginning, TRUE); + if (result) { + g_strfreev(args); + return result; + } + } else { + result = autocomplete_param_with_ac(input, "/vcard set name", vcard_name_ac, TRUE, previous); + + if (result) { + return result; + } + + result = autocomplete_param_with_ac(input, "/vcard set", vcard_set_ac, TRUE, previous); + + if (result) { + return result; + } + } + } + + result = autocomplete_param_with_ac(input, "/vcard add", vcard_element_ac, TRUE, previous); + + if (result) { + return result; + } + + if (window->type == WIN_MUC) { + char* unquoted = strip_arg_quotes(input); + + ProfMucWin* mucwin = (ProfMucWin*)window; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid); + + if (nick_ac) { + result = autocomplete_param_with_ac(unquoted, "/vcard get", nick_ac, TRUE, previous); + if (result) { + free(unquoted); + return result; + } + + result = autocomplete_param_with_ac(unquoted, "/vcard photo open", nick_ac, TRUE, previous); + if (result) { + free(unquoted); + return result; + } + + result = autocomplete_param_with_ac(unquoted, "/vcard photo save", nick_ac, TRUE, previous); + if (result) { + free(unquoted); + return result; + } + } + free(unquoted); + } else { + char* unquoted = strip_arg_quotes(input); + + result = autocomplete_param_with_func(unquoted, "/vcard get", roster_contact_autocomplete, previous, NULL); + if (result) { + free(unquoted); + return result; + } + + result = autocomplete_param_with_func(unquoted, "/vcard photo open", roster_contact_autocomplete, previous, NULL); + if (result) { + free(unquoted); + return result; + } + + result = autocomplete_param_with_func(unquoted, "/vcard photo save", roster_contact_autocomplete, previous, NULL); + if (result) { + free(unquoted); + return result; + } + free(unquoted); + } + + result = autocomplete_param_with_ac(input, "/vcard photo", vcard_photo_ac, TRUE, previous); + + if (result) { + return result; + } + + result = autocomplete_param_with_ac(input, "/vcard", vcard_ac, TRUE, previous); + + if (result) { + return result; + } + + return result; +} diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index 91346fce..4263ac22 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1268,7 +1268,8 @@ static struct cmd_t command_defs[] = { "/time all|console|chat|muc|config|private|xml off", "/time statusbar set ", "/time statusbar off", - "/time lastactivity set ") + "/time lastactivity set ", + "/time vcard set ") CMD_DESC( "Configure time display preferences. " "Time formats are strings supported by g_date_time_format. " @@ -1291,7 +1292,8 @@ static struct cmd_t command_defs[] = { { "statusbar set ", "Change time format in statusbar." }, { "statusbar off", "Do not show time in status bar." }, { "lastactivity set ", "Change time format for last activity." }, - { "all set ", "Set time for: console, chat, muc, config, private and xml windows." }, + { "vcard set ", "Change the time format used to display time/dates in vCard (such as birthdays)" }, + { "all set ", "Set time for: console, chat, muc, config, private, and xml windows." }, { "all off", "Do not show time for: console, chat, muc, config, private and xml windows." }) CMD_EXAMPLES( "/time console set %H:%M:%S", @@ -1582,6 +1584,124 @@ static struct cmd_t command_defs[] = { "/autoconnect off") }, + { "/vcard", + parse_args, 0, 7, NULL, + CMD_SUBFUNCS( + {"add", cmd_vcard_add}, + {"remove", cmd_vcard_remove}, + {"get", cmd_vcard_get}, + {"set", cmd_vcard_set}, + {"photo", cmd_vcard_photo}, + {"refresh", cmd_vcard_refresh}, + {"save", cmd_vcard_save}) + CMD_MAINFUNC(cmd_vcard) + CMD_TAGS( + CMD_TAG_CHAT, + CMD_TAG_GROUPCHAT) + CMD_SYN( + "/vcard get []", + "/vcard photo open []", + "/vcard photo save [output ] [index ]", + "/vcard set fullname ", + "/vcard set name family ", + "/vcard set name given ", + "/vcard set name middle ", + "/vcard set name prefix ", + "/vcard set name suffix ", + "/vcard set []", + "/vcard set pobox ", + "/vcard set extaddr ", + "/vcard set street ", + "/vcard set locality ", + "/vcard set region ", + "/vcard set pocode ", + "/vcard set country ", + "/vcard set type domestic|international", + "/vcard set home on|off", + "/vcard set work on|off", + "/vcard set voice on|off", + "/vcard set fax on|off", + "/vcard set pager on|off", + "/vcard set msg on|off", + "/vcard set cell on|off", + "/vcard set video on|off", + "/vcard set bbs on|off", + "/vcard set modem on|off", + "/vcard set isdn on|off", + "/vcard set pcs on|off", + "/vcard set preferred on|off", + "/vcard set parcel on|off", + "/vcard set postal on|off", + "/vcard set internet on|off", + "/vcard set x400 on|off", + "/vcard add nickname ", + "/vcard add birthday ", + "/vcard add address", + "/vcard add tel ", + "/vcard add email ", + "/vcard add jid ", + "/vcard add title ", + "/vcard add role <role>", + "/vcard add note <note>", + "/vcard add url <url>", + "/vcard remove <index>", + "/vcard refresh", + "/vcard save") + CMD_DESC( + "Read your vCard or a user's vCard, get a user's avatar via their vCard, or modify your vCard. If no arguments are given, your vCard will be displayed in a new window, or an existing vCard window.") + CMD_ARGS( + { "get [<nick|contact>]", "Get your vCard, if a nickname/contact is provided, get that user's vCard" }, + { "photo open <nick|contact> [<index>]", "Download a user's photo from their vCard to a file, and open it. If index is not specified, download the first photo (usually avatar) from their vCard" }, + { "photo save <nick|contact>", "Download a user's photo from their vCard to a file. If index is not specified, download the first photo (usually avatar) from their vCard. If output is not specified, download the photo to profanity's photos directory." }, + { "photo open-self [<index>]", "Download a photo from your vCard to a file, and open it. If index is not specified, download the first photo (usually avatar) from your vCard" }, + { "photo save-self", "Download a photo from your vCard to a file. If index is not specified, download the first photo (usually avatar) from your vCard. If output is not specified, download the photo to profanity's photos directory. Same arguments as `photo open`" }, + { "set fullname <fullname>", "Set your vCard's fullname to the specified value" }, + { "set name family <family>", "Set your vCard's family name to the specified value" }, + { "set name given <given>", "Set your vCard's given name to the specified value" }, + { "set name middle <middle>", "Set your vCard's middle name to the specified value" }, + { "set name prefix <prefix>", "Set your vCard's prefix name to the specified value" }, + { "set name suffix <suffix>", "Set your vCard's suffix name to the specified value" }, + { "set <index> [<value>]", "Set the main field in a element in your vCard to the specified value, or if no value was specified, modify the field in an editor, This only works in elements that have one field." }, + { "set <index> pobox <value>", "Set the P.O. box in an address element in your vCard to the specified value." }, + { "set <index> extaddr <value>", "Set the extended address in an address element in your vCard to the specified value." }, + { "set <index> street <value>", "Set the street in an address element in your vCard to the specified value." }, + { "set <index> locality <value>", "Set the locality in an address element in your vCard to the specified value." }, + { "set <index> region <value>", "Set the region in an address element in your vCard to the specified value." }, + { "set <index> pocode <value>", "Set the P.O. code in an address element in your vCard to the specified value." }, + { "set <index> type domestic|international", "Set the type in an address element in your vCard to either domestic or international." }, + { "set <index> home on|off", "Set the home option in an element in your vCard. (address, telephone, e-mail only)" }, + { "set <index> work on|off", "Set the work option in an element in your vCard. (address, telephone, e-mail only)" }, + { "set <index> voice on|off", "Set the voice option in a telephone element in your vCard." }, + { "set <index> fax on|off", "Set the fax option in a telephone element in your vCard." }, + { "set <index> pager on|off", "Set the pager option in a telephone element in your vCard." }, + { "set <index> msg on|off", "Set the message option in a telephone element in your vCard." }, + { "set <index> cell on|off", "Set the cellphone option in a telephone element in your vCard." }, + { "set <index> video on|off", "Set the video option in a telephone element in your vCard." }, + { "set <index> bbs on|off", "Set the BBS option in a telephone element in your vCard." }, + { "set <index> modem on|off", "Set the modem option in a telephone element in your vCard." }, + { "set <index> isdn on|off", "Set the ISDN option in a telephone element in your vCard." }, + { "set <index> pcs on|off", "Set the PCS option in a telephone element in your vCard." }, + { "set <index> preferred on|off", "Set the preferred option in an element in your vCard. (address, telephone, e-mail only)" }, + { "set <index> parcel on|off", "Set the parcel option in an address element in your vCard." }, + { "set <index> postal on|off", "Set the postal option in an address element in your vCard." }, + { "set <index> internet on|off", "Set the internet option in an e-mail address in your vCard." }, + { "set <index> x400 on|off", "Set the X400 option in an e-mail address in your vCard." }, + { "add nickname <nickname>", "Add a nickname to your vCard" }, + { "add birthday <date>", "Add a birthday date to your vCard" }, + { "add address", "Add an address to your vCard" }, + { "add tel <number>", "Add a telephone number to your vCard" }, + { "add email <userid>", "Add an e-mail address to your vCard" }, + { "add jid <jid>", "Add a Jabber ID to your vCard" }, + { "add title <title>", "Add a title to your vCard" }, + { "add role <role>", "Add a role to your vCard" }, + { "add note <note>", "Add a note to your vCard" }, + { "add url <url>", "Add a URL to your vCard" }, + { "remove <index>", "Remove a element in your vCard by index" }, + { "refresh", "Refreshes the local copy of the current account's vCard (undoes all your unpublished modifications)" }, + { "save", "Save changes to the server" }) + CMD_NOEXAMPLES + }, + { "/vercheck", parse_args, 0, 1, NULL, CMD_NOSUBFUNCS @@ -2559,7 +2679,8 @@ static struct cmd_t command_defs[] = { { "avatar", cmd_executable_avatar }, { "urlopen", cmd_executable_urlopen }, { "urlsave", cmd_executable_urlsave }, - { "editor", cmd_executable_editor }) + { "editor", cmd_executable_editor }, + { "vcard_photo", cmd_executable_vcard_photo }) CMD_NOMAINFUNC CMD_TAGS( CMD_TAG_DISCOVERY) @@ -2568,7 +2689,9 @@ static struct cmd_t command_defs[] = { "/executable urlopen set <cmdtemplate>", "/executable urlopen default", "/executable urlsave set <cmdtemplate>", - "/executable urlsave default") + "/executable urlsave default", + "/executable vcard_photo set <cmdtemplate>", + "/executable vcard_photo default") CMD_DESC( "Configure executable that should be called upon a certain command.") CMD_ARGS( @@ -2577,7 +2700,9 @@ static struct cmd_t command_defs[] = { { "urlopen default", "Restore to default settings." }, { "urlsave set", "Set executable that is run by /url save. Takes a command template that replaces %u and %p with the URL and path respectively." }, { "urlsave default", "Use the built-in download method for saving." }, - { "editor set", "Set editor to be used with /editor. Needs a terminal editor or a script to run a graphical editor." }) + { "editor set", "Set editor to be used with /editor. Needs a terminal editor or a script to run a graphical editor." }, + { "vcard_photo set", "Set executable that is run by /vcard photo open. Takes a command template that replaces %p with the path" }, + { "vcard_photo default", "Restore to default settings." }) CMD_EXAMPLES( "/executable avatar xdg-open", "/executable urlopen set \"xdg-open %u\"", @@ -2586,6 +2711,7 @@ static struct cmd_t command_defs[] = { "/executable urlsave set \"wget %u -O %p\"", "/executable urlsave set \"curl %u -o %p\"", "/executable urlsave default", + "/executable vcard_photo set \"feh %p\"", "/executable editor set vim") }, diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 9978e889..420bc0f6 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -91,6 +91,7 @@ #include "xmpp/chat_session.h" #include "xmpp/avatar.h" #include "xmpp/stanza.h" +#include "xmpp/vcard_funcs.h" #ifdef HAVE_LIBOTR #include "otr/otr.h" @@ -5580,6 +5581,25 @@ cmd_time(ProfWin* window, const char* const command, gchar** args) cons_bad_cmd_usage(command); return TRUE; } + } else if (g_strcmp0(args[0], "vcard") == 0) { + if (args[1] == NULL) { + char* format = prefs_get_string(PREF_TIME_VCARD); + cons_show("vCard time format: %s", format); + g_free(format); + return TRUE; + } else if (g_strcmp0(args[1], "set") == 0 && args[2] != NULL) { + prefs_set_string(PREF_TIME_VCARD, args[2]); + cons_show("vCard time format set to '%s'.", args[2]); + ui_redraw(); + return TRUE; + } else if (g_strcmp0(args[1], "off") == 0) { + cons_show("vCard time cannot be disabled."); + ui_redraw(); + return TRUE; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } } else { cons_bad_cmd_usage(command); return TRUE; @@ -9638,6 +9658,23 @@ cmd_executable_editor(ProfWin* window, const char* const command, gchar** args) return TRUE; } +gboolean +cmd_executable_vcard_photo(ProfWin* window, const char* const command, gchar** args) +{ + if (g_strcmp0(args[1], "set") == 0 && args[2] != NULL) { + prefs_set_string(PREF_VCARD_PHOTO_CMD, args[2]); + cons_show("`vcard photo open` command set to invoke '%s'", args[2]); + } else if (g_strcmp0(args[1], "default") == 0) { + prefs_set_string(PREF_VCARD_PHOTO_CMD, NULL); + char* cmd = prefs_get_string(PREF_VCARD_PHOTO_CMD); + cons_show("`vcard photo open` command set to invoke '%s' (default)", cmd); + g_free(cmd); + } else { + cons_bad_cmd_usage(command); + } + + return TRUE; +} gboolean cmd_mam(ProfWin* window, const char* const command, gchar** args) { @@ -9818,3 +9855,956 @@ cmd_mood(ProfWin* window, const char* const command, gchar** args) return TRUE; } + +gboolean +cmd_vcard(ProfWin* window, const char* const command, gchar** args) +{ + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + ProfVcardWin* vcardwin = wins_get_vcard(); + + if (vcardwin) { + ui_focus_win((ProfWin*)vcardwin); + } else { + vcardwin = (ProfVcardWin*)vcard_user_create_win(); + ui_focus_win((ProfWin*)vcardwin); + } + vcardwin_update(); + return TRUE; +} + +gboolean +cmd_vcard_add(ProfWin* window, const char* const command, gchar** args) +{ + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + cons_show_error("Memory allocation failed."); + return TRUE; + } + + struct tm tm; + gchar* type = args[1]; + gchar* value = args[2]; + + if (g_strcmp0(type, "nickname") == 0) { + element->type = VCARD_NICKNAME; + + element->nickname = strdup(value); + } else if (g_strcmp0(type, "birthday") == 0) { + element->type = VCARD_BIRTHDAY; + + memset(&tm, 0, sizeof(struct tm)); + if (!strptime(value, "%Y-%m-%d", &tm)) { + cons_show_error("Error parsing ISO8601 date."); + free(element); + return TRUE; + } + element->birthday = g_date_time_new_local(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, 0, 0, 0); + } else if (g_strcmp0(type, "tel") == 0) { + element->type = VCARD_TELEPHONE; + if (value) { + element->telephone.number = strdup(value); + } + } else if (g_strcmp0(type, "address") == 0) { + element->type = VCARD_ADDRESS; + } else if (g_strcmp0(type, "email") == 0) { + element->type = VCARD_EMAIL; + if (value) { + element->email.userid = strdup(value); + } + } else if (g_strcmp0(type, "jid") == 0) { + element->type = VCARD_JID; + if (value) { + element->jid = strdup(value); + } + } else if (g_strcmp0(type, "title") == 0) { + element->type = VCARD_TITLE; + if (value) { + element->title = strdup(value); + } + } else if (g_strcmp0(type, "role") == 0) { + element->type = VCARD_ROLE; + if (value) { + element->role = strdup(value); + } + } else if (g_strcmp0(type, "note") == 0) { + element->type = VCARD_NOTE; + if (value) { + element->note = strdup(value); + } + } else if (g_strcmp0(type, "url") == 0) { + element->type = VCARD_URL; + if (value) { + element->url = strdup(value); + } + } else { + cons_bad_cmd_usage(command); + free(element); + return TRUE; + } + + vcard_user_add_element(element); + vcardwin_update(); + return TRUE; +} + +gboolean +cmd_vcard_remove(ProfWin* window, const char* const command, gchar** args) +{ + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + if (args[1]) { + vcard_user_remove_element(atoi(args[1])); + cons_show("Removed element at index %d", atoi(args[1])); + vcardwin_update(); + } else { + cons_bad_cmd_usage(command); + } + return TRUE; +} + +gboolean +cmd_vcard_get(ProfWin* window, const char* const command, gchar** args) +{ + char* user = args[1]; + xmpp_ctx_t* const ctx = connection_get_ctx(); + + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + if (user) { + // get the JID when in MUC window + if (window->type == WIN_MUC) { + ProfMucWin* mucwin = (ProfMucWin*)window; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + + if (muc_anonymity_type(mucwin->roomjid) == MUC_ANONYMITY_TYPE_NONANONYMOUS) { + // non-anon muc: get the user's jid and send vcard request to them + Occupant* occupant = muc_roster_item(mucwin->roomjid, user); + Jid* jid_occupant = jid_create(occupant->jid); + + vcard_print(ctx, window, jid_occupant->barejid); + jid_destroy(jid_occupant); + } else { + // anon muc: send the vcard request through the MUC's server + GString* full_jid = g_string_new(mucwin->roomjid); + g_string_append(full_jid, "/"); + g_string_append(full_jid, user); + + vcard_print(ctx, window, full_jid->str); + + g_string_free(full_jid, TRUE); + } + } else { + char* jid = roster_barejid_from_name(user); + if (!jid) { + cons_bad_cmd_usage(command); + return TRUE; + } + + vcard_print(ctx, window, jid); + } + } else { + if (window->type == WIN_CHAT) { + ProfChatWin* chatwin = (ProfChatWin*)window; + assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK); + + vcard_print(ctx, window, chatwin->barejid); + } else { + vcard_print(ctx, window, NULL); + } + } + + return TRUE; +} + +gboolean +cmd_vcard_photo(ProfWin* window, const char* const command, gchar** args) +{ + char* operation = args[1]; + char* user = args[2]; + + xmpp_ctx_t* const ctx = connection_get_ctx(); + + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + gboolean jidless = (g_strcmp0(operation, "open-self") == 0 || g_strcmp0(operation, "save-self") == 0); + + if (!operation || (!jidless && !user)) { + cons_bad_cmd_usage(command); + return TRUE; + } + + char* jid = NULL; + char* filepath = NULL; + int index = 0; + + if (!jidless) { + if (window->type == WIN_MUC) { + ProfMucWin* mucwin = (ProfMucWin*)window; + assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK); + + if (muc_anonymity_type(mucwin->roomjid) == MUC_ANONYMITY_TYPE_NONANONYMOUS) { + // non-anon muc: get the user's jid and send vcard request to them + Occupant* occupant = muc_roster_item(mucwin->roomjid, user); + Jid* jid_occupant = jid_create(occupant->jid); + + jid = g_strdup(jid_occupant->barejid); + jid_destroy(jid_occupant); + } else { + // anon muc: send the vcard request through the MUC's server + GString* full_jid = g_string_new(mucwin->roomjid); + g_string_append(full_jid, "/"); + g_string_append(full_jid, user); + + jid = full_jid->str; + + g_string_free(full_jid, FALSE); + } + } else { + char* jid_temp = roster_barejid_from_name(user); + if (!jid_temp) { + cons_bad_cmd_usage(command); + return TRUE; + } else { + jid = g_strdup(jid_temp); + } + } + } + if (!g_strcmp0(operation, "open")) { + // if an index is provided + if (args[3]) { + vcard_photo(ctx, jid, NULL, atoi(args[3]), TRUE); + } else { + vcard_photo(ctx, jid, NULL, -1, TRUE); + } + } else if (!g_strcmp0(operation, "save")) { + // arguments + if (g_strv_length(args) > 2) { + gchar* opt_keys[] = { "output", "index", NULL }; + gboolean parsed; + + GHashTable* options = parse_options(&args[3], opt_keys, &parsed); + if (!parsed) { + cons_bad_cmd_usage(command); + options_destroy(options); + return TRUE; + } + + filepath = g_hash_table_lookup(options, "output"); + if (!filepath) { + filepath = NULL; + } + + char* index_str = g_hash_table_lookup(options, "index"); + if (!index_str) { + index = -1; + } else { + index = atoi(index_str); + } + + options_destroy(options); + } else { + filepath = NULL; + index = -1; + } + + vcard_photo(ctx, jid, filepath, index, FALSE); + } else if (!g_strcmp0(operation, "open-self")) { + // if an index is provided + if (args[2]) { + vcard_photo(ctx, NULL, NULL, atoi(args[2]), TRUE); + } else { + vcard_photo(ctx, NULL, NULL, -1, TRUE); + } + } else if (!g_strcmp0(operation, "save-self")) { + // arguments + if (g_strv_length(args) > 2) { + gchar* opt_keys[] = { "output", "index", NULL }; + gboolean parsed; + + GHashTable* options = parse_options(&args[2], opt_keys, &parsed); + if (!parsed) { + cons_bad_cmd_usage(command); + options_destroy(options); + return TRUE; + } + + filepath = g_hash_table_lookup(options, "output"); + if (!filepath) { + filepath = NULL; + } + + char* index_str = g_hash_table_lookup(options, "index"); + if (!index_str) { + index = -1; + } else { + index = atoi(index_str); + } + + options_destroy(options); + } else { + filepath = NULL; + index = -1; + } + + vcard_photo(ctx, NULL, filepath, index, FALSE); + } else { + cons_bad_cmd_usage(command); + } + + if (!jidless) { + g_free(jid); + } + return TRUE; +} + +gboolean +cmd_vcard_refresh(ProfWin* window, const char* const command, gchar** args) +{ + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + vcard_user_refresh(); + vcardwin_update(); + return TRUE; +} + +gboolean +cmd_vcard_set(ProfWin* window, const char* const command, gchar** args) +{ + char* key = args[1]; + char* value = args[2]; + + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + if (!key) { + cons_bad_cmd_usage(command); + return TRUE; + } + + gboolean is_num = TRUE; + for (int i = 0; i < strlen(key); i++) { + if (!isdigit((int)key[i])) { + is_num = FALSE; + break; + } + } + + if (g_strcmp0(key, "fullname") == 0 && value) { + vcard_user_set_fullname(value); + cons_show("User vCard's full name has been set"); + } else if (g_strcmp0(key, "name") == 0 && value) { + char* value2 = args[3]; + + if (!value2) { + cons_bad_cmd_usage(command); + return TRUE; + } + + if (g_strcmp0(value, "family") == 0) { + vcard_user_set_name_family(value2); + cons_show("User vCard's family name has been set"); + } else if (g_strcmp0(value, "given") == 0) { + vcard_user_set_name_given(value2); + cons_show("User vCard's given name has been set"); + } else if (g_strcmp0(value, "middle") == 0) { + vcard_user_set_name_middle(value2); + cons_show("User vCard's middle name has been set"); + } else if (g_strcmp0(value, "prefix") == 0) { + vcard_user_set_name_prefix(value2); + cons_show("User vCard's prefix name has been set"); + } else if (g_strcmp0(value, "suffix") == 0) { + vcard_user_set_name_suffix(value2); + cons_show("User vCard's suffix name has been set"); + } + } else if (is_num) { + char* value2 = args[3]; + struct tm tm; + + vcard_element_t* element = vcard_user_get_element_index(atoi(key)); + + if (!element) { + cons_bad_cmd_usage(command); + return TRUE; + } + + if (!value2 || !value) { + // Set the main field of element at index <key> to <value>, or from an editor + + switch (element->type) { + case VCARD_NICKNAME: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->nickname, &editor_value)) { + return TRUE; + } + + if (element->nickname) { + free(element->nickname); + } + element->nickname = editor_value; + } else { + if (element->nickname) { + free(element->nickname); + } + element->nickname = strdup(value); + } + break; + case VCARD_BIRTHDAY: + memset(&tm, 0, sizeof(struct tm)); + if (!strptime(value, "%Y-%m-%d", &tm)) { + cons_show_error("Error parsing ISO8601 date."); + return TRUE; + } + + if (element->birthday) { + g_date_time_unref(element->birthday); + } + element->birthday = g_date_time_new_local(tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, 0, 0, 0); + break; + case VCARD_TELEPHONE: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->telephone.number, &editor_value)) { + return TRUE; + } + + if (element->telephone.number) { + free(element->telephone.number); + } + element->telephone.number = editor_value; + } else { + if (element->telephone.number) { + free(element->telephone.number); + } + element->telephone.number = strdup(value); + } + + break; + case VCARD_EMAIL: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->email.userid, &editor_value)) { + return TRUE; + } + + if (element->email.userid) { + free(element->email.userid); + } + element->email.userid = editor_value; + } else { + if (element->email.userid) { + free(element->email.userid); + } + element->email.userid = strdup(value); + } + break; + case VCARD_JID: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->jid, &editor_value)) { + return TRUE; + } + + if (element->jid) { + free(element->jid); + } + element->jid = editor_value; + } else { + if (element->jid) { + free(element->jid); + } + element->jid = strdup(value); + } + break; + case VCARD_TITLE: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->title, &editor_value)) { + return TRUE; + } + + if (element->title) { + free(element->title); + } + element->title = editor_value; + } else { + if (element->title) { + free(element->title); + } + element->title = strdup(value); + } + break; + case VCARD_ROLE: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->role, &editor_value)) { + return TRUE; + } + + if (element->role) { + free(element->role); + } + element->role = editor_value; + } else { + if (element->role) { + free(element->role); + } + element->role = strdup(value); + } + break; + case VCARD_NOTE: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->note, &editor_value)) { + return TRUE; + } + + if (element->note) { + free(element->note); + } + element->note = editor_value; + } else { + if (element->note) { + free(element->note); + } + element->note = strdup(value); + } + break; + case VCARD_URL: + if (!value) { + gchar* editor_value; + if (get_message_from_editor(element->url, &editor_value)) { + return TRUE; + } + + if (element->url) { + free(element->url); + } + element->url = editor_value; + } else { + if (element->url) { + free(element->url); + } + element->url = strdup(value); + } + break; + default: + cons_show_error("Element unsupported"); + } + } else if (value) { + if (g_strcmp0(value, "pobox") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.pobox, &editor_value)) { + return TRUE; + } + + if (element->address.pobox) { + free(element->address.pobox); + } + element->address.pobox = editor_value; + } else { + if (element->address.pobox) { + free(element->address.pobox); + } + element->address.pobox = strdup(value2); + } + } else if (g_strcmp0(value, "extaddr") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.extaddr, &editor_value)) { + return TRUE; + } + + if (element->address.extaddr) { + free(element->address.extaddr); + } + element->address.extaddr = editor_value; + } else { + if (element->address.extaddr) { + free(element->address.extaddr); + } + element->address.extaddr = strdup(value2); + } + } else if (g_strcmp0(value, "street") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.street, &editor_value)) { + return TRUE; + } + + if (element->address.street) { + free(element->address.street); + } + element->address.street = editor_value; + } else { + if (element->address.street) { + free(element->address.street); + } + element->address.street = strdup(value2); + } + } else if (g_strcmp0(value, "locality") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.locality, &editor_value)) { + return TRUE; + } + + if (element->address.locality) { + free(element->address.locality); + } + element->address.locality = editor_value; + } else { + if (element->address.locality) { + free(element->address.locality); + } + element->address.locality = strdup(value2); + } + } else if (g_strcmp0(value, "region") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.region, &editor_value)) { + return TRUE; + } + + if (element->address.region) { + free(element->address.region); + } + element->address.region = editor_value; + } else { + if (element->address.region) { + free(element->address.region); + } + element->address.region = strdup(value2); + } + } else if (g_strcmp0(value, "pocode") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.pcode, &editor_value)) { + return TRUE; + } + + if (element->address.pcode) { + free(element->address.pcode); + } + element->address.pcode = editor_value; + } else { + if (element->address.pcode) { + free(element->address.pcode); + } + element->address.pcode = strdup(value2); + } + } else if (g_strcmp0(value, "country") == 0 && element->type == VCARD_ADDRESS) { + if (!value2) { + gchar* editor_value; + if (get_message_from_editor(element->address.country, &editor_value)) { + return TRUE; + } + + if (element->address.country) { + free(element->address.country); + } + element->address.country = editor_value; + } else { + if (element->address.country) { + free(element->address.country); + } + element->address.country = strdup(value2); + } + } else if (g_strcmp0(value, "type") == 0 && element->type == VCARD_ADDRESS) { + if (g_strcmp0(value2, "domestic") == 0) { + element->address.options &= ~VCARD_INTL; + element->address.options |= VCARD_DOM; + } else if (g_strcmp0(value2, "international") == 0) { + element->address.options &= ~VCARD_DOM; + element->address.options |= VCARD_INTL; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "home") == 0) { + switch (element->type) { + case VCARD_ADDRESS: + if (g_strcmp0(value2, "on") == 0) { + element->address.options |= VCARD_HOME; + } else if (g_strcmp0(value2, "off") == 0) { + element->address.options &= ~VCARD_HOME; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + case VCARD_TELEPHONE: + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_HOME; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_HOME; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + case VCARD_EMAIL: + if (g_strcmp0(value2, "on") == 0) { + element->email.options |= VCARD_HOME; + } else if (g_strcmp0(value2, "off") == 0) { + element->email.options &= ~VCARD_HOME; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + default: + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "work") == 0) { + switch (element->type) { + case VCARD_ADDRESS: + if (g_strcmp0(value2, "on") == 0) { + element->address.options |= VCARD_WORK; + } else if (g_strcmp0(value2, "off") == 0) { + element->address.options &= ~VCARD_WORK; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + case VCARD_TELEPHONE: + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_WORK; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_WORK; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + case VCARD_EMAIL: + if (g_strcmp0(value2, "on") == 0) { + element->email.options |= VCARD_WORK; + } else if (g_strcmp0(value2, "off") == 0) { + element->email.options &= ~VCARD_WORK; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + default: + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "voice") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_VOICE; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_VOICE; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "fax") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_FAX; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_FAX; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "pager") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_PAGER; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_PAGER; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "msg") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_MSG; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_MSG; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "cell") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_CELL; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_CELL; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "video") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_VIDEO; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_VIDEO; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "bbs") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_BBS; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_BBS; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "modem") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_MODEM; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_MODEM; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "isdn") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_ISDN; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_ISDN; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "pcs") == 0 && element->type == VCARD_TELEPHONE) { + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_TEL_PCS; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_TEL_PCS; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "preferred") == 0) { + switch (element->type) { + case VCARD_ADDRESS: + if (g_strcmp0(value2, "on") == 0) { + element->address.options |= VCARD_PREF; + } else if (g_strcmp0(value2, "off") == 0) { + element->address.options &= ~VCARD_PREF; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + case VCARD_TELEPHONE: + if (g_strcmp0(value2, "on") == 0) { + element->telephone.options |= VCARD_PREF; + } else if (g_strcmp0(value2, "off") == 0) { + element->telephone.options &= ~VCARD_PREF; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + case VCARD_EMAIL: + if (g_strcmp0(value2, "on") == 0) { + element->email.options |= VCARD_PREF; + } else if (g_strcmp0(value2, "off") == 0) { + element->email.options &= ~VCARD_PREF; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + break; + default: + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "parcel") == 0 && element->type == VCARD_ADDRESS) { + if (g_strcmp0(value2, "on") == 0) { + element->address.options |= VCARD_PARCEL; + } else if (g_strcmp0(value2, "off") == 0) { + element->address.options &= ~VCARD_PARCEL; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "postal") == 0 && element->type == VCARD_ADDRESS) { + if (g_strcmp0(value2, "on") == 0) { + element->address.options |= VCARD_POSTAL; + } else if (g_strcmp0(value2, "off") == 0) { + element->address.options &= ~VCARD_POSTAL; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "internet") == 0 && element->type == VCARD_EMAIL) { + if (g_strcmp0(value2, "on") == 0) { + element->email.options |= VCARD_EMAIL_INTERNET; + } else if (g_strcmp0(value2, "off") == 0) { + element->email.options &= ~VCARD_EMAIL_INTERNET; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else if (g_strcmp0(value, "x400") == 0 && element->type == VCARD_EMAIL) { + if (g_strcmp0(value2, "on") == 0) { + element->email.options |= VCARD_EMAIL_X400; + } else if (g_strcmp0(value2, "off") == 0) { + element->email.options &= ~VCARD_EMAIL_X400; + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + } else { + cons_bad_cmd_usage(command); + return TRUE; + } + + vcardwin_update(); + return TRUE; +} + +gboolean +cmd_vcard_save(ProfWin* window, const char* const command, gchar** args) +{ + if (connection_get_status() != JABBER_CONNECTED) { + cons_show("You are not currently connected."); + return TRUE; + } + + vcard_user_save(); + cons_show("User vCard uploaded"); + return TRUE; +} diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h index adc6793d..83594bdd 100644 --- a/src/command/cmd_funcs.h +++ b/src/command/cmd_funcs.h @@ -246,6 +246,7 @@ gboolean cmd_executable_avatar(ProfWin* window, const char* const command, gchar gboolean cmd_executable_urlopen(ProfWin* window, const char* const command, gchar** args); gboolean cmd_executable_urlsave(ProfWin* window, const char* const command, gchar** args); gboolean cmd_executable_editor(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_executable_vcard_photo(ProfWin* window, const char* const command, gchar** args); gboolean cmd_mam(ProfWin* window, const char* const command, gchar** args); gboolean cmd_editor(ProfWin* window, const char* const command, gchar** args); gboolean cmd_correct_editor(ProfWin* window, const char* const command, gchar** args); @@ -253,5 +254,13 @@ gboolean cmd_silence(ProfWin* window, const char* const command, gchar** args); gboolean cmd_register(ProfWin* window, const char* const command, gchar** args); gboolean cmd_mood(ProfWin* window, const char* const command, gchar** args); gboolean cmd_stamp(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_add(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_remove(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_get(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_photo(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_refresh(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_set(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_vcard_save(ProfWin* window, const char* const command, gchar** args); #endif diff --git a/src/config/files.h b/src/config/files.h index 16739291..1fcee58e 100644 --- a/src/config/files.h +++ b/src/config/files.h @@ -60,6 +60,7 @@ #define DIR_DOWNLOADS "downloads" #define DIR_EDITOR "editor" #define DIR_CERTS "certs" +#define DIR_PHOTOS "photos" void files_create_directories(void); diff --git a/src/config/preferences.c b/src/config/preferences.c index dec92e7f..122c36c6 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -1770,6 +1770,7 @@ _get_group(preference_t pref) case PREF_TIME_XMLCONSOLE: case PREF_TIME_STATUSBAR: case PREF_TIME_LASTACTIVITY: + case PREF_TIME_VCARD: case PREF_ROSTER: case PREF_ROSTER_OFFLINE: case PREF_ROSTER_RESOURCE: @@ -1850,6 +1851,7 @@ _get_group(preference_t pref) case PREF_AVATAR_CMD: case PREF_URL_OPEN_CMD: case PREF_URL_SAVE_CMD: + case PREF_VCARD_PHOTO_CMD: return PREF_GROUP_EXECUTABLES; case PREF_AUTOAWAY_CHECK: case PREF_AUTOAWAY_MODE: @@ -2029,6 +2031,8 @@ _get_key(preference_t pref) return "time.statusbar"; case PREF_TIME_LASTACTIVITY: return "time.lastactivity"; + case PREF_TIME_VCARD: + return "time.vcard"; case PREF_ROSTER: return "roster"; case PREF_ROSTER_OFFLINE: @@ -2159,6 +2163,8 @@ _get_key(preference_t pref) return "log"; case PREF_MOOD: return "mood"; + case PREF_VCARD_PHOTO_CMD: + return "vcard.photo.cmd"; default: return NULL; } @@ -2269,6 +2275,8 @@ _get_default_string(preference_t pref) return "%H:%M"; case PREF_TIME_LASTACTIVITY: return "%d/%m/%y %H:%M:%S"; + case PREF_TIME_VCARD: + return "%d/%m/%y"; case PREF_PGP_LOG: return "on"; case PREF_CONSOLE_MUC: @@ -2293,6 +2301,8 @@ _get_default_string(preference_t pref) return "xdg-open"; case PREF_URL_OPEN_CMD: return "xdg-open %u"; + case PREF_VCARD_PHOTO_CMD: + return "xdg-open %p"; case PREF_COMPOSE_EDITOR: { gchar* editor = getenv("EDITOR"); diff --git a/src/config/preferences.h b/src/config/preferences.h index 2ef6204f..5e856573 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -105,6 +105,7 @@ typedef enum { PREF_TIME_XMLCONSOLE, PREF_TIME_STATUSBAR, PREF_TIME_LASTACTIVITY, + PREF_TIME_VCARD, PREF_STATUSES, PREF_STATUSES_CONSOLE, PREF_STATUSES_CHAT, @@ -181,6 +182,7 @@ typedef enum { PREF_NOTIFY_ROOM_OFFLINE, PREF_OX_LOG, PREF_MOOD, + PREF_VCARD_PHOTO_CMD, } preference_t; typedef struct prof_alias_t diff --git a/src/event/common.c b/src/event/common.c index 5c17ca9a..84bc0d58 100644 --- a/src/event/common.c +++ b/src/event/common.c @@ -41,6 +41,7 @@ #include "xmpp/roster_list.h" #include "xmpp/muc.h" #include "xmpp/xmpp.h" +#include "xmpp/vcard_funcs.h" #include "database.h" #include "tools/bookmark_ignore.h" @@ -73,6 +74,7 @@ ev_disconnect_cleanup(void) #endif log_database_close(); bookmark_ignore_on_disconnect(); + vcard_user_free(); } gboolean diff --git a/src/event/server_events.c b/src/event/server_events.c index 57f73c06..df796448 100644 --- a/src/event/server_events.c +++ b/src/event/server_events.c @@ -61,6 +61,7 @@ #include "xmpp/chat_session.h" #include "xmpp/roster_list.h" #include "xmpp/avatar.h" +#include "xmpp/vcard_funcs.h" #ifdef HAVE_LIBOTR #include "otr/otr.h" @@ -101,7 +102,7 @@ sv_ev_login_account_success(char* account_name, gboolean secured) #endif log_database_init(account); - + vcard_user_refresh(); avatar_pep_subscribe(); ui_handle_login_account_success(account, secured); diff --git a/src/ui/console.c b/src/ui/console.c index 822e6d3e..e7b656f5 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -1517,6 +1517,10 @@ cons_time_setting(void) char* pref_time_lastactivity = prefs_get_string(PREF_TIME_LASTACTIVITY); cons_show("Time last activity (/time) : %s", pref_time_lastactivity); g_free(pref_time_lastactivity); + + char* pref_time_vcard = prefs_get_string(PREF_TIME_VCARD); + cons_show("Time vCard (/time) : %s", pref_time_vcard); + g_free(pref_time_vcard); } void @@ -2262,6 +2266,10 @@ cons_executable_setting(void) gchar* editor = prefs_get_string(PREF_COMPOSE_EDITOR); cons_show("Default '/editor' command (/executable editor) : %s", editor); g_free(editor); + + gchar* vcard_cmd = prefs_get_string(PREF_VCARD_PHOTO_CMD); + cons_show("Default '/vcard photo open' command (/executable vcard_photo) : %s", vcard_cmd); + g_free(vcard_cmd); } void diff --git a/src/ui/ui.h b/src/ui/ui.h index a3377690..78632aa0 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -230,6 +230,11 @@ char* confwin_get_string(ProfConfWin* confwin); void xmlwin_show(ProfXMLWin* xmlwin, const char* const msg); char* xmlwin_get_string(ProfXMLWin* xmlwin); +// vCard window +void vcardwin_show_vcard_config(ProfVcardWin* vcardwin); +char* vcardwin_get_string(ProfVcardWin* vcardwin); +void vcardwin_update(void); + // Input window char* inp_readline(void); void inp_nonblocking(gboolean reset); @@ -371,6 +376,7 @@ ProfWin* win_create_muc(const char* const roomjid); ProfWin* win_create_config(const char* const title, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata); ProfWin* win_create_private(const char* const fulljid); ProfWin* win_create_plugin(const char* const plugin_name, const char* const tag); +ProfWin* win_create_vcard(vCard* vcard); void win_update_virtual(ProfWin* window); void win_free(ProfWin* window); gboolean win_notify_remind(ProfWin* window); @@ -396,6 +402,8 @@ void win_show_occupant(ProfWin* window, Occupant* occupant); void win_show_occupant_info(ProfWin* window, const char* const room, Occupant* occupant); void win_show_contact(ProfWin* window, PContact contact); void win_show_info(ProfWin* window, PContact contact); +void win_show_vcard(ProfWin* window, vCard* vcard); + void win_clear(ProfWin* window); char* win_get_tab_identifier(ProfWin* window); char* win_to_string(ProfWin* window); diff --git a/src/ui/vcardwin.c b/src/ui/vcardwin.c new file mode 100644 index 00000000..a49f240c --- /dev/null +++ b/src/ui/vcardwin.c @@ -0,0 +1,75 @@ +/* + * vcardwin.c + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org> + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see <https://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#include "ui/ui.h" +#include "ui/window_list.h" +#include "xmpp/vcard.h" + +void +vcardwin_show_vcard_config(ProfVcardWin* vcardwin) +{ + ProfWin* window = &vcardwin->window; + + win_clear(window); + win_show_vcard(window, vcardwin->vcard); + + win_println(window, THEME_DEFAULT, "-", "Use '/vcard save' to save changes."); + win_println(window, THEME_DEFAULT, "-", "Use '/help vcard' for more information."); +} + +char* +vcardwin_get_string(ProfVcardWin* vcardwin) +{ + GString* string = g_string_new("vCard: "); + char* jid = connection_get_barejid(); + g_string_append(string, jid); + + if (vcardwin->vcard && vcardwin->vcard->modified) { + g_string_append(string, " (modified)"); + } + + free(jid); + return g_string_free(string, FALSE); +} + +void +vcardwin_update(void) +{ + ProfVcardWin* win = wins_get_vcard(); + + if (win) { + vcardwin_show_vcard_config(win); + } +} diff --git a/src/ui/win_types.h b/src/ui/win_types.h index 70d226b0..b7cd3519 100644 --- a/src/ui/win_types.h +++ b/src/ui/win_types.h @@ -52,6 +52,7 @@ #include "tools/autocomplete.h" #include "ui/buffer.h" #include "xmpp/chat_state.h" +#include "xmpp/vcard.h" #define LAYOUT_SPLIT_MEMCHECK 12345671 #define PROFCHATWIN_MEMCHECK 22374522 @@ -60,6 +61,7 @@ #define PROFCONFWIN_MEMCHECK 64334685 #define PROFXMLWIN_MEMCHECK 87333463 #define PROFPLUGINWIN_MEMCHECK 43434777 +#define PROFVCARDWIN_MEMCHECK 68947523 typedef enum { FIELD_HIDDEN, @@ -140,7 +142,8 @@ typedef enum { WIN_CONFIG, WIN_PRIVATE, WIN_XML, - WIN_PLUGIN + WIN_PLUGIN, + WIN_VCARD } win_type_t; typedef struct prof_win_t @@ -239,4 +242,11 @@ typedef struct prof_plugin_win_t unsigned long memcheck; } ProfPluginWin; +typedef struct prof_vcard_win_t +{ + ProfWin window; + vCard* vcard; + unsigned long memcheck; +} ProfVcardWin; + #endif diff --git a/src/ui/window.c b/src/ui/window.c index e25002d6..fbff3374 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -281,6 +281,19 @@ win_create_plugin(const char* const plugin_name, const char* const tag) return &new_win->window; } +ProfWin* +win_create_vcard(vCard* vcard) +{ + ProfVcardWin* new_win = malloc(sizeof(ProfVcardWin)); + new_win->window.type = WIN_VCARD; + new_win->window.layout = _win_create_simple_layout(); + + new_win->vcard = vcard; + new_win->memcheck = PROFVCARDWIN_MEMCHECK; + + return &new_win->window; +} + char* win_get_title(ProfWin* window) { @@ -351,7 +364,22 @@ win_get_title(ProfWin* window) assert(pluginwin->memcheck == PROFPLUGINWIN_MEMCHECK); return strdup(pluginwin->tag); } + if (window->type == WIN_VCARD) { + ProfVcardWin* vcardwin = (ProfVcardWin*)window; + assert(vcardwin->memcheck == PROFVCARDWIN_MEMCHECK); + GString* title = g_string_new("vCard "); + char* jid = connection_get_barejid(); + + g_string_append(title, jid); + + if (vcardwin->vcard->modified) { + g_string_append(title, " *"); + } + + free(jid); + return g_string_free(title, FALSE); + } return NULL; } @@ -474,6 +502,11 @@ win_to_string(ProfWin* window) g_string_free(gstring, FALSE); return res; } + case WIN_VCARD: + { + ProfVcardWin* vcardwin = (ProfVcardWin*)window; + return vcardwin_get_string(vcardwin); + } default: return NULL; } @@ -1082,6 +1115,162 @@ win_show_info(ProfWin* window, PContact contact) g_list_free(ordered_resources); } +void +win_show_vcard(ProfWin* window, vCard* vcard) +{ + GList* pointer; + int index = 0; + + if (vcard->fullname) { + win_println(window, THEME_DEFAULT, "!", "Full name: %s", vcard->fullname); + } + if (vcard->name.family || vcard->name.given || vcard->name.middle || vcard->name.prefix || vcard->name.suffix) { + win_println(window, THEME_DEFAULT, "!", "Name: "); + if (vcard->name.family) { + win_println(window, THEME_DEFAULT, "!", " Family: %s", vcard->name.family); + } + if (vcard->name.given) { + win_println(window, THEME_DEFAULT, "!", " Given: %s", vcard->name.given); + } + if (vcard->name.middle) { + win_println(window, THEME_DEFAULT, "!", " Middle: %s", vcard->name.middle); + } + if (vcard->name.prefix) { + win_println(window, THEME_DEFAULT, "!", " Prefix: %s", vcard->name.prefix); + } + if (vcard->name.suffix) { + win_println(window, THEME_DEFAULT, "!", " Suffix: %s", vcard->name.suffix); + } + } + index = 0; + for (pointer = g_queue_peek_head_link(vcard->elements); pointer != NULL; pointer = pointer->next, index++) { + assert(pointer->data != NULL); + vcard_element_t* element = (vcard_element_t*)pointer->data; + + switch (element->type) { + case VCARD_NICKNAME: + { + win_println(window, THEME_DEFAULT, "!", "[%d] Nickname: %s", index, element->nickname); + break; + } + case VCARD_PHOTO: + { + if (element->photo.external) { + win_println(window, THEME_DEFAULT, "!", "[%d] Photo, External value: %s", index, element->photo.extval); + } else { + win_println(window, THEME_DEFAULT, "!", "[%d] Photo (%s), size: %zu", index, element->photo.type, element->photo.length); + } + break; + } + case VCARD_BIRTHDAY: + { + char* date_format = prefs_get_string(PREF_TIME_VCARD); + gchar* date = g_date_time_format(element->birthday, date_format); + g_free(date_format); + + assert(date != NULL); + win_println(window, THEME_DEFAULT, "!", "[%d] Birthday: %s", index, date); + g_free(date); + break; + } + case VCARD_ADDRESS: + { + // Print the header with flags + win_println(window, THEME_DEFAULT, "!", "[%d] Address%s%s%s%s%s%s%s", index, + (element->address.options & VCARD_HOME) ? " [home]" : "", + (element->address.options & VCARD_WORK) == VCARD_WORK ? " [work]" : "", + (element->address.options & VCARD_POSTAL) == VCARD_POSTAL ? " [postal]" : "", + (element->address.options & VCARD_PARCEL) == VCARD_PARCEL ? " [parcel]" : "", + (element->address.options & VCARD_INTL) == VCARD_INTL ? " [international]" : "", + (element->address.options & VCARD_DOM) == VCARD_DOM ? " [domestic]" : "", + (element->address.options & VCARD_PREF) == VCARD_PREF ? " [preferred]" : ""); + + if (element->address.pobox) { + win_println(window, THEME_DEFAULT, "!", " P.O. Box: %s", element->address.pobox); + } + if (element->address.extaddr) { + win_println(window, THEME_DEFAULT, "!", " Extended address: %s", element->address.extaddr); + } + if (element->address.street) { + win_println(window, THEME_DEFAULT, "!", " Street: %s", element->address.street); + } + if (element->address.locality) { + win_println(window, THEME_DEFAULT, "!", " Locality: %s", element->address.locality); + } + if (element->address.region) { + win_println(window, THEME_DEFAULT, "!", " Region: %s", element->address.region); + } + if (element->address.pcode) { + win_println(window, THEME_DEFAULT, "!", " Postal code: %s", element->address.pcode); + } + if (element->address.country) { + win_println(window, THEME_DEFAULT, "!", " Country: %s", element->address.country); + } + break; + } + case VCARD_TELEPHONE: + { + // Print the header with flags + win_println(window, THEME_DEFAULT, "!", "[%d] Telephone%s%s%s%s%s%s%s%s%s%s%s%s%s", index, + (element->telephone.options & VCARD_HOME) ? " [home]" : "", + (element->telephone.options & VCARD_WORK) == VCARD_WORK ? " [work]" : "", + (element->telephone.options & VCARD_TEL_VOICE) == VCARD_TEL_VOICE ? " [voice]" : "", + (element->telephone.options & VCARD_TEL_FAX) == VCARD_TEL_FAX ? " [fax]" : "", + (element->telephone.options & VCARD_TEL_PAGER) == VCARD_TEL_PAGER ? " [pager]" : "", + (element->telephone.options & VCARD_TEL_MSG) == VCARD_TEL_MSG ? " [msg]" : "", + (element->telephone.options & VCARD_TEL_CELL) == VCARD_TEL_CELL ? " [cell]" : "", + (element->telephone.options & VCARD_TEL_VIDEO) == VCARD_TEL_VIDEO ? " [video]" : "", + (element->telephone.options & VCARD_TEL_BBS) == VCARD_TEL_BBS ? " [bbs]" : "", + (element->telephone.options & VCARD_TEL_MODEM) == VCARD_TEL_MODEM ? " [modem]" : "", + (element->telephone.options & VCARD_TEL_ISDN) == VCARD_TEL_ISDN ? " [isdn]" : "", + (element->telephone.options & VCARD_TEL_PCS) == VCARD_TEL_PCS ? " [pcs]" : "", + (element->telephone.options & VCARD_PREF) == VCARD_PREF ? " [preferred]" : ""); + if (element->telephone.number) { + win_println(window, THEME_DEFAULT, "!", " Number: %s", element->telephone.number); + } + break; + } + case VCARD_EMAIL: + { + // Print the header with flags + win_println(window, THEME_DEFAULT, "!", "[%d] E-mail%s%s%s%s%s", index, + (element->email.options & VCARD_HOME) ? " [home]" : "", + (element->email.options & VCARD_WORK) == VCARD_WORK ? " [work]" : "", + (element->email.options & VCARD_EMAIL_X400) == VCARD_EMAIL_X400 ? " [x400]" : "", + (element->email.options & VCARD_EMAIL_INTERNET) == VCARD_EMAIL_INTERNET ? " [internet]" : "", + (element->email.options & VCARD_PREF) == VCARD_PREF ? " [preferred]" : ""); + if (element->email.userid) { + win_println(window, THEME_DEFAULT, "!", " ID: %s", element->email.userid); + } + break; + } + case VCARD_JID: + { + win_println(window, THEME_DEFAULT, "!", "[%d] Jabber ID: %s", index, element->jid); + break; + } + case VCARD_TITLE: + { + win_println(window, THEME_DEFAULT, "!", "[%d] Title: %s", index, element->title); + break; + } + case VCARD_ROLE: + { + win_println(window, THEME_DEFAULT, "!", "[%d] Role: %s", index, element->role); + break; + } + case VCARD_NOTE: + { + win_println(window, THEME_DEFAULT, "!", "[%d] Note: %s", index, element->note); + break; + } + case VCARD_URL: + win_println(window, THEME_DEFAULT, "!", "[%d] URL: %s", index, element->url); + break; + } + } +} + void win_show_status_string(ProfWin* window, const char* const from, const char* const show, const char* const status, diff --git a/src/ui/window_list.c b/src/ui/window_list.c index 56370bca..cdb87de4 100644 --- a/src/ui/window_list.c +++ b/src/ui/window_list.c @@ -711,6 +711,18 @@ wins_new_plugin(const char* const plugin_name, const char* const tag) return newwin; } +ProfWin* +wins_new_vcard(vCard* vcard) +{ + GList* keys = g_hash_table_get_keys(windows); + int result = _wins_get_next_available_num(keys); + g_list_free(keys); + ProfWin* newwin = win_create_vcard(vcard); + g_hash_table_insert(windows, GINT_TO_POINTER(result), newwin); + + return newwin; +} + gboolean wins_do_notify_remind(void) { @@ -805,6 +817,27 @@ wins_get_xmlconsole(void) return NULL; } +ProfVcardWin* +wins_get_vcard(void) +{ + GList* values = g_hash_table_get_values(windows); + GList* curr = values; + + while (curr) { + ProfWin* window = curr->data; + if (window->type == WIN_VCARD) { + ProfVcardWin* vcardwin = (ProfVcardWin*)window; + assert(vcardwin->memcheck == PROFVCARDWIN_MEMCHECK); + g_list_free(values); + return vcardwin; + } + curr = g_list_next(curr); + } + + g_list_free(values); + return NULL; +} + GSList* wins_get_chat_recipients(void) { diff --git a/src/ui/window_list.h b/src/ui/window_list.h index bcb18d14..788248b9 100644 --- a/src/ui/window_list.h +++ b/src/ui/window_list.h @@ -46,6 +46,7 @@ ProfWin* wins_new_muc(const char* const roomjid); ProfWin* wins_new_config(const char* const roomjid, DataForm* form, ProfConfWinCallback submit, ProfConfWinCallback cancel, const void* userdata); ProfWin* wins_new_private(const char* const fulljid); ProfWin* wins_new_plugin(const char* const plugin_name, const char* const tag); +ProfWin* wins_new_vcard(vCard* vcard); gboolean wins_chat_exists(const char* const barejid); GList* wins_get_private_chats(const char* const roomjid); @@ -61,6 +62,7 @@ ProfConfWin* wins_get_conf(const char* const roomjid); ProfPrivateWin* wins_get_private(const char* const fulljid); ProfPluginWin* wins_get_plugin(const char* const tag); ProfXMLWin* wins_get_xmlconsole(void); +ProfVcardWin* wins_get_vcard(void); void wins_close_plugin(char* tag); diff --git a/src/xmpp/stanza.c b/src/xmpp/stanza.c index 6b8377b6..fc2fcdc6 100644 --- a/src/xmpp/stanza.c +++ b/src/xmpp/stanza.c @@ -2687,6 +2687,25 @@ stanza_create_avatar_metadata_publish_iq(xmpp_ctx_t* ctx, const char* img_data, return iq; } +xmpp_stanza_t* +stanza_create_vcard_request_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const stanza_id) +{ + xmpp_stanza_t* iq = xmpp_iq_new(ctx, STANZA_TYPE_GET, stanza_id); + xmpp_stanza_set_from(iq, connection_get_fulljid()); + if (jid) { + xmpp_stanza_set_to(iq, jid); + } + + xmpp_stanza_t* vcard = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(vcard, STANZA_NAME_VCARD); + xmpp_stanza_set_ns(vcard, STANZA_NS_VCARD); + + xmpp_stanza_add_child(iq, vcard); + xmpp_stanza_release(vcard); + + return iq; +} + xmpp_stanza_t* stanza_attach_correction(xmpp_ctx_t* ctx, xmpp_stanza_t* stanza, const char* const replace_id) { diff --git a/src/xmpp/stanza.h b/src/xmpp/stanza.h index 12c9a5ee..ddd46e9a 100644 --- a/src/xmpp/stanza.h +++ b/src/xmpp/stanza.h @@ -123,6 +123,7 @@ #define STANZA_NAME_MOOD "mood" #define STANZA_NAME_RECEIVED "received" #define STANZA_NAME_SENT "sent" +#define STANZA_NAME_VCARD "vCard" // error conditions #define STANZA_NAME_BAD_REQUEST "bad-request" @@ -249,6 +250,7 @@ #define STANZA_NS_MOOD_NOTIFY "http://jabber.org/protocol/mood+notify" #define STANZA_NS_STREAMS "http://etherx.jabber.org/streams" #define STANZA_NS_XMPP_STREAMS "urn:ietf:params:xml:ns:xmpp-streams" +#define STANZA_NS_VCARD "vcard-temp" #define STANZA_DATAFORM_SOFTWARE "urn:xmpp:dataforms:softwareinfo" @@ -413,6 +415,7 @@ void stanza_free_caps(XMPPCaps* caps); xmpp_stanza_t* stanza_create_avatar_retrieve_data_request(xmpp_ctx_t* ctx, const char* stanza_id, const char* const item_id, const char* const jid); xmpp_stanza_t* stanza_create_avatar_data_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len); xmpp_stanza_t* stanza_create_avatar_metadata_publish_iq(xmpp_ctx_t* ctx, const char* img_data, gsize len, int height, int width); +xmpp_stanza_t* stanza_create_vcard_request_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const stanza_id); xmpp_stanza_t* stanza_create_mam_iq(xmpp_ctx_t* ctx, const char* const jid, const char* const startdate, const char* const lastid); xmpp_stanza_t* stanza_change_password(xmpp_ctx_t* ctx, const char* const user, const char* const password); xmpp_stanza_t* stanza_register_new_account(xmpp_ctx_t* ctx, const char* const user, const char* const password); diff --git a/src/xmpp/vcard.c b/src/xmpp/vcard.c new file mode 100644 index 00000000..6d2f0b1e --- /dev/null +++ b/src/xmpp/vcard.c @@ -0,0 +1,1613 @@ +/* + * vcard.c + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org> + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see <https://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#include <assert.h> +#include <errno.h> +#include <glib.h> +#include <strophe.h> +#include <sys/stat.h> + +#include "xmpp/vcard.h" +#include "config/files.h" +#include "config/preferences.h" +#include "ui/ui.h" +#include "ui/window_list.h" +#include "xmpp/connection.h" +#include "xmpp/iq.h" +#include "xmpp/stanza.h" + +// Connected account's vCard +vCard* vcard_user = NULL; + +typedef struct +{ + vCard* vcard; + ProfWin* window; + + // for photo + int photo_index; + gboolean open; + char* filename; +} _userdata; + +static void +_free_vcard_element(void* velement) +{ + vcard_element_t* element = (vcard_element_t*)velement; + + switch (element->type) { + case VCARD_NICKNAME: + { + if (element->nickname) { + free(element->nickname); + } + break; + } + case VCARD_PHOTO: + { + if (element->photo.external) { + if (element->photo.extval) { + free(element->photo.extval); + } + } else { + if (element->photo.data) { + g_free(element->photo.data); + } + if (element->photo.type) { + free(element->photo.type); + } + } + break; + } + case VCARD_BIRTHDAY: + { + g_date_time_unref(element->birthday); + break; + } + case VCARD_ADDRESS: + { + if (element->address.pobox) { + free(element->address.pobox); + } + if (element->address.extaddr) { + free(element->address.extaddr); + } + if (element->address.street) { + free(element->address.street); + } + if (element->address.locality) { + free(element->address.locality); + } + if (element->address.region) { + free(element->address.region); + } + if (element->address.pcode) { + free(element->address.pcode); + } + if (element->address.country) { + free(element->address.country); + } + break; + } + case VCARD_TELEPHONE: + { + if (element->telephone.number) { + free(element->telephone.number); + } + break; + } + case VCARD_EMAIL: + { + if (element->email.userid) { + free(element->email.userid); + } + break; + } + case VCARD_JID: + { + if (element->jid) { + free(element->jid); + } + break; + } + case VCARD_TITLE: + { + if (element->title) { + free(element->title); + } + break; + } + case VCARD_ROLE: + { + if (element->role) { + free(element->role); + } + break; + } + case VCARD_NOTE: + { + if (element->note) { + free(element->note); + } + break; + } + case VCARD_URL: + if (element->url) { + free(element->url); + } + break; + } + + free(element); +} + +void +vcard_free_full(vCard* vcard) +{ + if (!vcard) { + return; + } + + // Free the fullname element + if (vcard->fullname) { + free(vcard->fullname); + } + + // Free the name element + if (vcard->name.family) { + free(vcard->name.family); + } + if (vcard->name.given) { + free(vcard->name.given); + } + if (vcard->name.middle) { + free(vcard->name.middle); + } + if (vcard->name.prefix) { + free(vcard->name.prefix); + } + if (vcard->name.suffix) { + free(vcard->name.suffix); + } + + // Clear the elements queue + g_queue_clear_full(vcard->elements, _free_vcard_element); +} + +vCard* +vcard_new(void) +{ + vCard* vcard = calloc(1, sizeof(vCard)); + + if (!vcard) { + return NULL; + } + + vcard->elements = g_queue_new(); + + return vcard; +} + +void +vcard_free(vCard* vcard) +{ + if (!vcard) { + return; + } + + g_queue_free(vcard->elements); + free(vcard); +} + +static void +_free_userdata(_userdata* data) +{ + vcard_free_full(data->vcard); + vcard_free(data->vcard); + + if (data->filename) { + free(data->filename); + } + + free(data); +} + +// Function must be called with <vCard> root element +gboolean +vcard_parse(xmpp_stanza_t* vcard_xml, vCard* vcard) +{ + // 2 bits + // 01 = fullname is set + // 10 = name is set + // Fullname and name can only be set once, if another + // fullname, or name element is found, then it will be + // ignored + char flags = 0; + + if (!vcard_xml) { + // vCard XML is null, stop + return FALSE; + } + + // Pointer to children of the vCard element + xmpp_stanza_t* child_pointer = xmpp_stanza_get_children(vcard_xml); + + // Connection context, for freeing elements allocated by Strophe + const xmpp_ctx_t* ctx = connection_get_ctx(); + + // Pointer to the children of the children + xmpp_stanza_t* child_pointer2 = NULL; + + for (; child_pointer != NULL; child_pointer = xmpp_stanza_get_next(child_pointer)) { + if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "FN") && !(flags & 1)) { + vcard->fullname = stanza_text_strdup(child_pointer); + flags |= 1; + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "N") && !(flags & 2)) { + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "FAMILY"); + vcard->name.family = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "GIVEN"); + vcard->name.given = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "MIDDLE"); + vcard->name.middle = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "PREFIX"); + vcard->name.prefix = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "SUFFIX"); + vcard->name.suffix = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + flags |= 2; + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "NICKNAME")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_NICKNAME; + element->nickname = stanza_text_strdup(child_pointer); + + if (!element->nickname) { + // Invaild element, free and do not push + free(element); + continue; + } + + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "PHOTO")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_PHOTO; + + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "EXTVAL"); + if (child_pointer2) { + element->photo.external = TRUE; + element->photo.extval = stanza_text_strdup(child_pointer2); + } else { + element->photo.external = FALSE; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "BINVAL"); + if (!child_pointer2) { + // No EXTVAL or BINVAL, invalid photo, skipping + free(element); + continue; + } + char* photo_base64 = xmpp_stanza_get_text(child_pointer2); + if (!photo_base64) { + free(element); + continue; + } + element->photo.data = g_base64_decode(photo_base64, &element->photo.length); + xmpp_free(ctx, photo_base64); + + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "TYPE"); + if (!child_pointer2) { + // No TYPE, invalid photo, skipping + if (element->photo.data) { + g_free(element->photo.data); + } + free(element); + continue; + } + + element->photo.type = stanza_text_strdup(child_pointer2); + } + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "BDAY")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_BIRTHDAY; + + char* bday_text = xmpp_stanza_get_text(child_pointer); + if (!bday_text) { + free(element); + continue; + } + + // Check if the birthday string is a date/time or date only string + char* check_pointer = bday_text; + gboolean is_datetime = FALSE; + for (; *check_pointer != 0; check_pointer++) { + if (*check_pointer == 'T' || *check_pointer == 't' || *check_pointer == ' ') { + is_datetime = TRUE; + break; + } + } + + if (!is_datetime) { + // glib doesn't parse ISO8601 date only strings, so we're gonna + // add a time string to make it parse + GString* date_string = g_string_new(bday_text); + g_string_append(date_string, "T00:00:00Z"); + + element->birthday = g_date_time_new_from_iso8601(date_string->str, NULL); + + g_string_free(date_string, TRUE); + } else { + element->birthday = g_date_time_new_from_iso8601(bday_text, NULL); + } + + xmpp_free(ctx, bday_text); + + if (!element->birthday) { + // Invalid birthday ISO 8601 date, skipping + free(element); + continue; + } + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "ADR")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_ADDRESS; + if (xmpp_stanza_get_child_by_name(child_pointer, "HOME")) { + element->address.options |= VCARD_HOME; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "WORK")) { + element->address.options |= VCARD_WORK; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "POSTAL")) { + element->address.options |= VCARD_POSTAL; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "PARCEL")) { + element->address.options |= VCARD_PARCEL; + } + + if (xmpp_stanza_get_child_by_name(child_pointer, "DOM")) { + element->address.options |= VCARD_DOM; + } else if (xmpp_stanza_get_child_by_name(child_pointer, "INTL")) { + element->address.options |= VCARD_INTL; + } + + if (xmpp_stanza_get_child_by_name(child_pointer, "PREF")) { + element->address.options |= VCARD_PREF; + } + + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "POBOX"); + element->address.pobox = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "EXTADD"); + element->address.extaddr = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "STREET"); + element->address.street = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "LOCALITY"); + element->address.locality = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "REGION"); + element->address.region = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "PCODE"); + element->address.pcode = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "CTRY"); + element->address.country = child_pointer2 ? stanza_text_strdup(child_pointer2) : NULL; + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "TEL")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_TELEPHONE; + if (xmpp_stanza_get_child_by_name(child_pointer, "HOME")) { + element->telephone.options |= VCARD_HOME; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "WORK")) { + element->telephone.options |= VCARD_WORK; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "VOICE")) { + element->telephone.options |= VCARD_TEL_VOICE; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "FAX")) { + element->telephone.options |= VCARD_TEL_FAX; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "PAGER")) { + element->telephone.options |= VCARD_TEL_PAGER; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "MSG")) { + element->telephone.options |= VCARD_TEL_MSG; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "CELL")) { + element->telephone.options |= VCARD_TEL_CELL; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "VIDEO")) { + element->telephone.options |= VCARD_TEL_VIDEO; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "BBS")) { + element->telephone.options |= VCARD_TEL_BBS; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "MODEM")) { + element->telephone.options |= VCARD_TEL_MODEM; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "ISDN")) { + element->telephone.options |= VCARD_TEL_ISDN; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "PCS")) { + element->telephone.options |= VCARD_TEL_PCS; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "PREF")) { + element->telephone.options |= VCARD_PREF; + } + + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "NUMBER"); + if (!child_pointer2) { + // No NUMBER, invalid telephone, skipping + free(element); + continue; + } + element->telephone.number = stanza_text_strdup(child_pointer2); + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "EMAIL")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_EMAIL; + if (xmpp_stanza_get_child_by_name(child_pointer, "HOME")) { + element->email.options |= VCARD_HOME; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "WORK")) { + element->email.options |= VCARD_WORK; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "INTERNET")) { + element->email.options |= VCARD_EMAIL_INTERNET; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "X400")) { + element->email.options |= VCARD_EMAIL_X400; + } + if (xmpp_stanza_get_child_by_name(child_pointer, "PREF")) { + element->email.options |= VCARD_PREF; + } + + child_pointer2 = xmpp_stanza_get_child_by_name(child_pointer, "USERID"); + if (!child_pointer2) { + // No USERID, invalid email, skipping + free(element); + continue; + } + element->email.userid = stanza_text_strdup(child_pointer2); + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "JABBERID")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_JID; + element->jid = stanza_text_strdup(child_pointer); + + if (!element->jid) { + // Invalid element, free and do not push + free(element); + continue; + } + + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "TITLE")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_TITLE; + element->title = stanza_text_strdup(child_pointer); + + if (!element->title) { + // Invalid element, free and do not push + free(element); + continue; + } + + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "ROLE")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_ROLE; + element->role = stanza_text_strdup(child_pointer); + + if (!element->role) { + // Invalid element, free and do not push + free(element); + continue; + } + + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "NOTE")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_NOTE; + element->note = stanza_text_strdup(child_pointer); + + if (!element->note) { + // Invalid element, free and do not push + free(element); + continue; + } + + g_queue_push_tail(vcard->elements, element); + } else if (!g_strcmp0(xmpp_stanza_get_name(child_pointer), "URL")) { + vcard_element_t* element = calloc(1, sizeof(vcard_element_t)); + if (!element) { + // Allocation failed + continue; + } + + element->type = VCARD_URL; + element->url = stanza_text_strdup(child_pointer); + + if (!element->note) { + // Invalid element, free and do not push + free(element); + continue; + } + + g_queue_push_tail(vcard->elements, element); + } + } + return TRUE; +} + +xmpp_stanza_t* +vcard_to_xml(xmpp_ctx_t* const ctx, vCard* vcard) +{ + if (!vcard || !ctx) { + return NULL; + } + + xmpp_stanza_t* vcard_stanza = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(vcard_stanza, STANZA_NAME_VCARD); + xmpp_stanza_set_ns(vcard_stanza, STANZA_NS_VCARD); + + if (vcard->fullname) { + // <FN> element + xmpp_stanza_t* fn = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(fn, "FN"); + + xmpp_stanza_t* fn_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(fn_text, vcard->fullname); + xmpp_stanza_add_child(fn, fn_text); + xmpp_stanza_release(fn_text); + + xmpp_stanza_add_child(vcard_stanza, fn); + xmpp_stanza_release(fn); + } + + if (vcard->name.family || vcard->name.given || vcard->name.middle || vcard->name.prefix || vcard->name.suffix) { + // <NAME> element + xmpp_stanza_t* name = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name, "NAME"); + + if (vcard->name.family) { + xmpp_stanza_t* name_family = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name_family, "FAMILY"); + + xmpp_stanza_t* name_family_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(name_family_text, vcard->name.family); + xmpp_stanza_add_child(name_family, name_family_text); + xmpp_stanza_release(name_family_text); + + xmpp_stanza_add_child(name, name_family); + xmpp_stanza_release(name_family); + } + + if (vcard->name.given) { + xmpp_stanza_t* name_given = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name_given, "GIVEN"); + + xmpp_stanza_t* name_given_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(name_given_text, vcard->name.given); + xmpp_stanza_add_child(name_given, name_given_text); + xmpp_stanza_release(name_given_text); + + xmpp_stanza_add_child(name, name_given); + xmpp_stanza_release(name_given); + } + + if (vcard->name.middle) { + xmpp_stanza_t* name_middle = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name_middle, "MIDDLE"); + + xmpp_stanza_t* name_middle_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(name_middle_text, vcard->name.middle); + xmpp_stanza_add_child(name_middle, name_middle_text); + xmpp_stanza_release(name_middle_text); + + xmpp_stanza_add_child(name, name_middle); + xmpp_stanza_release(name_middle); + } + + if (vcard->name.prefix) { + xmpp_stanza_t* name_prefix = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name_prefix, "PREFIX"); + + xmpp_stanza_t* name_prefix_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(name_prefix_text, vcard->name.prefix); + xmpp_stanza_add_child(name_prefix, name_prefix_text); + xmpp_stanza_release(name_prefix_text); + + xmpp_stanza_add_child(name, name_prefix); + xmpp_stanza_release(name_prefix); + } + + if (vcard->name.suffix) { + xmpp_stanza_t* name_suffix = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(name_suffix, "SUFFIX"); + + xmpp_stanza_t* name_suffix_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(name_suffix_text, vcard->name.suffix); + xmpp_stanza_add_child(name_suffix, name_suffix_text); + xmpp_stanza_release(name_suffix_text); + + xmpp_stanza_add_child(name, name_suffix); + xmpp_stanza_release(name_suffix); + } + + xmpp_stanza_add_child(vcard_stanza, name); + xmpp_stanza_release(name); + } + + GList* pointer = g_queue_peek_head_link(vcard->elements); + + for (; pointer != NULL; pointer = pointer->next) { + assert(pointer->data != NULL); + vcard_element_t* element = (vcard_element_t*)pointer->data; + + switch (element->type) { + case VCARD_NICKNAME: + { + // <NICKNAME> element + xmpp_stanza_t* nickname = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(nickname, "NICKNAME"); + + xmpp_stanza_t* nickname_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(nickname_text, element->nickname); + xmpp_stanza_add_child(nickname, nickname_text); + xmpp_stanza_release(nickname_text); + + xmpp_stanza_add_child(vcard_stanza, nickname); + xmpp_stanza_release(nickname); + break; + } + case VCARD_PHOTO: + { + // <PHOTO> element + xmpp_stanza_t* photo = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(photo, "PHOTO"); + + if (element->photo.external) { + xmpp_stanza_t* extval = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(extval, "EXTVAL"); + + xmpp_stanza_t* extval_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(extval_text, element->photo.extval); + xmpp_stanza_add_child(extval, extval_text); + xmpp_stanza_release(extval_text); + + xmpp_stanza_add_child(photo, extval); + xmpp_stanza_release(extval); + } else { + xmpp_stanza_t* binval = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(binval, "BINVAL"); + + gchar* base64 = g_base64_encode(element->photo.data, element->photo.length); + xmpp_stanza_t* binval_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(binval_text, base64); + g_free(base64); + xmpp_stanza_add_child(binval, binval_text); + xmpp_stanza_release(binval_text); + + xmpp_stanza_add_child(photo, binval); + xmpp_stanza_release(binval); + + xmpp_stanza_t* type = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(type, "TYPE"); + + xmpp_stanza_t* type_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(type_text, element->photo.type); + xmpp_stanza_add_child(type, type_text); + xmpp_stanza_release(type_text); + + xmpp_stanza_add_child(photo, type); + xmpp_stanza_release(type); + } + + xmpp_stanza_add_child(vcard_stanza, photo); + xmpp_stanza_release(photo); + break; + } + case VCARD_BIRTHDAY: + { + // <BDAY> element + xmpp_stanza_t* birthday = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(birthday, "BDAY"); + + gchar* bday_text = g_date_time_format(element->birthday, "%Y-%m-%d"); + xmpp_stanza_t* birthday_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(birthday_text, bday_text); + g_free(bday_text); + xmpp_stanza_add_child(birthday, birthday_text); + xmpp_stanza_release(birthday_text); + + xmpp_stanza_add_child(vcard_stanza, birthday); + xmpp_stanza_release(birthday); + break; + } + case VCARD_ADDRESS: + { + // <ADR> element + xmpp_stanza_t* address = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(address, "ADR"); + + // Options + if (element->address.options & VCARD_HOME) { + xmpp_stanza_t* home = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(home, "HOME"); + + xmpp_stanza_add_child(address, home); + xmpp_stanza_release(home); + } + if ((element->address.options & VCARD_WORK) == VCARD_WORK) { + xmpp_stanza_t* work = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(work, "WORK"); + + xmpp_stanza_add_child(address, work); + xmpp_stanza_release(work); + } + if ((element->address.options & VCARD_POSTAL) == VCARD_POSTAL) { + xmpp_stanza_t* postal = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(postal, "POSTAL"); + + xmpp_stanza_add_child(address, postal); + xmpp_stanza_release(postal); + } + if ((element->address.options & VCARD_PARCEL) == VCARD_PARCEL) { + xmpp_stanza_t* parcel = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(parcel, "PARCEL"); + + xmpp_stanza_add_child(address, parcel); + xmpp_stanza_release(parcel); + } + if ((element->address.options & VCARD_INTL) == VCARD_INTL) { + xmpp_stanza_t* intl = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(intl, "INTL"); + + xmpp_stanza_add_child(address, intl); + xmpp_stanza_release(intl); + } + if ((element->address.options & VCARD_DOM) == VCARD_DOM) { + xmpp_stanza_t* dom = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(dom, "DOM"); + + xmpp_stanza_add_child(address, dom); + xmpp_stanza_release(dom); + } + if ((element->address.options & VCARD_PREF) == VCARD_PREF) { + xmpp_stanza_t* pref = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pref, "PREF"); + + xmpp_stanza_add_child(address, pref); + xmpp_stanza_release(pref); + } + + if (element->address.pobox) { + xmpp_stanza_t* pobox = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pobox, "POBOX"); + + xmpp_stanza_t* pobox_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(pobox_text, element->address.pobox); + xmpp_stanza_add_child(pobox, pobox_text); + xmpp_stanza_release(pobox_text); + + xmpp_stanza_add_child(address, pobox); + xmpp_stanza_release(pobox); + } + if (element->address.extaddr) { + xmpp_stanza_t* extaddr = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(extaddr, "EXTADD"); + + xmpp_stanza_t* extaddr_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(extaddr_text, element->address.extaddr); + xmpp_stanza_add_child(extaddr, extaddr_text); + xmpp_stanza_release(extaddr_text); + + xmpp_stanza_add_child(address, extaddr); + xmpp_stanza_release(extaddr); + } + if (element->address.street) { + xmpp_stanza_t* street = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(street, "STREET"); + + xmpp_stanza_t* street_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(street_text, element->address.street); + xmpp_stanza_add_child(street, street_text); + xmpp_stanza_release(street_text); + + xmpp_stanza_add_child(address, street); + xmpp_stanza_release(street); + } + if (element->address.locality) { + xmpp_stanza_t* locality = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(locality, "LOCALITY"); + + xmpp_stanza_t* locality_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(locality_text, element->address.locality); + xmpp_stanza_add_child(locality, locality_text); + xmpp_stanza_release(locality_text); + + xmpp_stanza_add_child(address, locality); + xmpp_stanza_release(locality); + } + if (element->address.region) { + xmpp_stanza_t* region = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(region, "REGION"); + + xmpp_stanza_t* region_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(region_text, element->address.region); + xmpp_stanza_add_child(region, region_text); + xmpp_stanza_release(region_text); + + xmpp_stanza_add_child(address, region); + xmpp_stanza_release(region); + } + if (element->address.pcode) { + xmpp_stanza_t* pcode = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pcode, "PCODE"); + + xmpp_stanza_t* pcode_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(pcode_text, element->address.pcode); + xmpp_stanza_add_child(pcode, pcode_text); + xmpp_stanza_release(pcode_text); + + xmpp_stanza_add_child(address, pcode); + xmpp_stanza_release(pcode); + } + if (element->address.country) { + xmpp_stanza_t* ctry = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(ctry, "CTRY"); + + xmpp_stanza_t* ctry_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(ctry_text, element->address.country); + xmpp_stanza_add_child(ctry, ctry_text); + xmpp_stanza_release(ctry_text); + + xmpp_stanza_add_child(address, ctry); + xmpp_stanza_release(ctry); + } + + xmpp_stanza_add_child(vcard_stanza, address); + xmpp_stanza_release(address); + break; + } + case VCARD_TELEPHONE: + { + // <TEL> element + xmpp_stanza_t* tel = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(tel, "TEL"); + + // Options + if (element->telephone.options & VCARD_HOME) { + xmpp_stanza_t* home = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(home, "HOME"); + + xmpp_stanza_add_child(tel, home); + xmpp_stanza_release(home); + } + if ((element->telephone.options & VCARD_WORK) == VCARD_WORK) { + xmpp_stanza_t* work = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(work, "WORK"); + + xmpp_stanza_add_child(tel, work); + xmpp_stanza_release(work); + } + if ((element->telephone.options & VCARD_TEL_VOICE) == VCARD_TEL_VOICE) { + xmpp_stanza_t* voice = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(voice, "VOICE"); + + xmpp_stanza_add_child(tel, voice); + xmpp_stanza_release(voice); + } + if ((element->telephone.options & VCARD_TEL_FAX) == VCARD_TEL_FAX) { + xmpp_stanza_t* fax = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(fax, "FAX"); + + xmpp_stanza_add_child(tel, fax); + xmpp_stanza_release(fax); + } + if ((element->telephone.options & VCARD_TEL_PAGER) == VCARD_TEL_PAGER) { + xmpp_stanza_t* pager = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pager, "PAGER"); + + xmpp_stanza_add_child(tel, pager); + xmpp_stanza_release(pager); + } + if ((element->telephone.options & VCARD_TEL_MSG) == VCARD_TEL_MSG) { + xmpp_stanza_t* msg = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(msg, "MSG"); + + xmpp_stanza_add_child(tel, msg); + xmpp_stanza_release(msg); + } + if ((element->telephone.options & VCARD_TEL_CELL) == VCARD_TEL_CELL) { + xmpp_stanza_t* cell = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(cell, "CELL"); + + xmpp_stanza_add_child(tel, cell); + xmpp_stanza_release(cell); + } + if ((element->telephone.options & VCARD_TEL_VIDEO) == VCARD_TEL_VIDEO) { + xmpp_stanza_t* video = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(video, "VIDEO"); + + xmpp_stanza_add_child(tel, video); + xmpp_stanza_release(video); + } + if ((element->telephone.options & VCARD_TEL_BBS) == VCARD_TEL_BBS) { + xmpp_stanza_t* bbs = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(bbs, "BBS"); + + xmpp_stanza_add_child(tel, bbs); + xmpp_stanza_release(bbs); + } + if ((element->telephone.options & VCARD_TEL_MODEM) == VCARD_TEL_MODEM) { + xmpp_stanza_t* modem = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(modem, "MODEM"); + + xmpp_stanza_add_child(tel, modem); + xmpp_stanza_release(modem); + } + if ((element->telephone.options & VCARD_TEL_ISDN) == VCARD_TEL_ISDN) { + xmpp_stanza_t* isdn = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(isdn, "ISDN"); + + xmpp_stanza_add_child(tel, isdn); + xmpp_stanza_release(isdn); + } + if ((element->telephone.options & VCARD_TEL_PCS) == VCARD_TEL_PCS) { + xmpp_stanza_t* pcs = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pcs, "PCS"); + + xmpp_stanza_add_child(tel, pcs); + xmpp_stanza_release(pcs); + } + if ((element->telephone.options & VCARD_PREF) == VCARD_PREF) { + xmpp_stanza_t* pref = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pref, "PREF"); + + xmpp_stanza_add_child(tel, pref); + xmpp_stanza_release(pref); + } + + if (element->telephone.number) { + xmpp_stanza_t* number = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(number, "NUMBER"); + + xmpp_stanza_t* number_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(number_text, element->telephone.number); + xmpp_stanza_add_child(number, number_text); + xmpp_stanza_release(number_text); + + xmpp_stanza_add_child(tel, number); + xmpp_stanza_release(number); + } + xmpp_stanza_add_child(vcard_stanza, tel); + xmpp_stanza_release(tel); + break; + } + case VCARD_EMAIL: + { + xmpp_stanza_t* email = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(email, "EMAIL"); + + if (element->email.options & VCARD_HOME) { + xmpp_stanza_t* home = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(home, "HOME"); + + xmpp_stanza_add_child(email, home); + xmpp_stanza_release(home); + } + if ((element->email.options & VCARD_WORK) == VCARD_WORK) { + xmpp_stanza_t* work = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(work, "WORK"); + + xmpp_stanza_add_child(email, work); + xmpp_stanza_release(work); + } + if ((element->email.options & VCARD_EMAIL_X400) == VCARD_EMAIL_X400) { + xmpp_stanza_t* x400 = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(x400, "X400"); + + xmpp_stanza_add_child(email, x400); + xmpp_stanza_release(x400); + } + if ((element->email.options & VCARD_PREF) == VCARD_PREF) { + xmpp_stanza_t* pref = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(pref, "PREF"); + + xmpp_stanza_add_child(email, pref); + xmpp_stanza_release(pref); + } + + if (element->email.userid) { + xmpp_stanza_t* userid = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(userid, "USERID"); + + xmpp_stanza_t* userid_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(userid_text, element->email.userid); + xmpp_stanza_add_child(userid, userid_text); + xmpp_stanza_release(userid_text); + + xmpp_stanza_add_child(email, userid); + xmpp_stanza_release(userid); + } + + xmpp_stanza_add_child(vcard_stanza, email); + xmpp_stanza_release(email); + break; + } + case VCARD_JID: + { + xmpp_stanza_t* jid = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(jid, "JABBERID"); + + if (element->jid) { + xmpp_stanza_t* jid_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(jid_text, element->jid); + xmpp_stanza_add_child(jid, jid_text); + xmpp_stanza_release(jid_text); + } + + xmpp_stanza_add_child(vcard_stanza, jid); + xmpp_stanza_release(jid); + break; + } + case VCARD_TITLE: + { + xmpp_stanza_t* title = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(title, "TITLE"); + + if (element->title) { + xmpp_stanza_t* title_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(title_text, element->title); + xmpp_stanza_add_child(title, title_text); + xmpp_stanza_release(title_text); + } + + xmpp_stanza_add_child(vcard_stanza, title); + xmpp_stanza_release(title); + break; + } + case VCARD_ROLE: + { + xmpp_stanza_t* role = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(role, "ROLE"); + + if (element->role) { + xmpp_stanza_t* role_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(role_text, element->role); + xmpp_stanza_add_child(role, role_text); + xmpp_stanza_release(role_text); + } + + xmpp_stanza_add_child(vcard_stanza, role); + xmpp_stanza_release(role); + break; + } + case VCARD_NOTE: + { + xmpp_stanza_t* note = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(note, "NOTE"); + + if (element->note) { + xmpp_stanza_t* note_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(note_text, element->note); + xmpp_stanza_add_child(note, note_text); + xmpp_stanza_release(note_text); + } + + xmpp_stanza_add_child(vcard_stanza, note); + xmpp_stanza_release(note); + break; + } + case VCARD_URL: + { + xmpp_stanza_t* url = xmpp_stanza_new(ctx); + xmpp_stanza_set_name(url, "URL"); + + if (element->url) { + xmpp_stanza_t* url_text = xmpp_stanza_new(ctx); + xmpp_stanza_set_text(url_text, element->url); + xmpp_stanza_add_child(url, url_text); + xmpp_stanza_release(url_text); + } + + xmpp_stanza_add_child(vcard_stanza, url); + xmpp_stanza_release(url); + break; + } + } + } + + return vcard_stanza; +} + +static int +_vcard_print_result(xmpp_stanza_t* const stanza, void* userdata) +{ + _userdata* data = (_userdata*)userdata; + const char* from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); + + if (from) { + win_println(data->window, THEME_DEFAULT, "!", "vCard for %s", from); + } else { + win_println(data->window, THEME_DEFAULT, "!", "This account's vCard"); + } + + xmpp_stanza_t* vcard_xml = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_VCARD); + if (!vcard_parse(vcard_xml, data->vcard)) { + return 1; + } + + win_show_vcard(data->window, data->vcard); + + return 1; +} + +void +vcard_print(xmpp_ctx_t* ctx, ProfWin* window, char* jid) +{ + if (!jid && vcard_user && vcard_user->modified) { + win_println(window, THEME_DEFAULT, "!", "This account's vCard (modified, `/vcard upload` to push)"); + win_show_vcard(window, vcard_user); + return; + } + + _userdata* data = calloc(1, sizeof(_userdata)); + data->vcard = vcard_new(); + if (!data || !data->vcard) { + if (data) { + free(data); + } + + cons_show("vCard allocation failed"); + return; + } + + data->window = window; + + char* id = connection_create_stanza_id(); + xmpp_stanza_t* iq = stanza_create_vcard_request_iq(ctx, jid, id); + + iq_id_handler_add(id, _vcard_print_result, (ProfIqFreeCallback)_free_userdata, data); + + free(id); + iq_send_stanza(iq); + xmpp_stanza_release(iq); +} + +static int +_vcard_photo_result(xmpp_stanza_t* const stanza, void* userdata) +{ + _userdata* data = (_userdata*)userdata; + const char* from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_FROM); + if (!from) { + from = xmpp_stanza_get_attribute(stanza, STANZA_ATTR_TO); + } + + vcard_element_photo_t* photo = NULL; + + xmpp_stanza_t* vcard_xml = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_VCARD); + if (!vcard_parse(vcard_xml, data->vcard)) { + return 1; + } + + if (data->photo_index < 0) { + GList* list_pointer; + for (list_pointer = g_queue_peek_head_link(data->vcard->elements); list_pointer != NULL; list_pointer = list_pointer->next) { + vcard_element_t* element = list_pointer->data; + + if (element->type != VCARD_PHOTO) { + continue; + } else { + photo = &element->photo; + } + } + + if (photo == NULL) { + cons_show_error("No photo was found in vCard"); + return 1; + } + } else { + vcard_element_t* element = (vcard_element_t*)g_queue_peek_nth(data->vcard->elements, data->photo_index); + + if (element == NULL) { + cons_show_error("No element was found at index %d", data->photo_index); + return 1; + } else if (element->type != VCARD_PHOTO) { + cons_show_error("Element is not a photo"); + return 1; + } + + photo = &element->photo; + } + + if (photo->external) { + cons_show_error("Cannot handle external value: %s", photo->extval); + return 1; + } + + GString* filename; + + if (!data->filename) { + char* path = files_get_data_path(DIR_PHOTOS); + filename = g_string_new(path); + free(path); + g_string_append(filename, "/"); + + errno = 0; + int res = g_mkdir_with_parents(filename->str, S_IRWXU); + if (res == -1) { + const char* errmsg = strerror(errno); + if (errmsg) { + cons_show_error("Error creating directory %s: %s", filename->str, errmsg); + g_string_free(filename, TRUE); + return 1; + } else { + cons_show_error("Unknown error creating directory %s", filename->str); + g_string_free(filename, TRUE); + } + } + gchar* from1 = str_replace(from, "@", "_at_"); + gchar* from2 = str_replace(from1, "/", "_slash_"); + g_free(from1); + + g_string_append(filename, from2); + g_free(from2); + } else { + filename = g_string_new(data->filename); + } + + // check a few image types ourselves + // TODO: we could use /etc/mime-types + if (g_strcmp0(photo->type, "image/png") == 0 || g_strcmp0(photo->type, "img/png") == 0) { + g_string_append(filename, ".png"); + } else if (g_strcmp0(photo->type, "image/jpeg") == 0 || g_strcmp0(photo->type, "img/jpeg") == 0) { + g_string_append(filename, ".jpeg"); + } else if (g_strcmp0(photo->type, "image/webp") == 0 || g_strcmp0(photo->type, "img/webp") == 0) { + g_string_append(filename, ".webp"); + } + + GError* err = NULL; + + if (g_file_set_contents(filename->str, (gchar*)photo->data, photo->length, &err) == FALSE) { + cons_show_error("Unable to save photo: %s", err->message); + g_error_free(err); + g_string_free(filename, TRUE); + return 1; + } else { + cons_show("Photo saved as %s", filename->str); + } + + if (data->open) { + gchar** argv; + gint argc; + + gchar* cmdtemplate = prefs_get_string(PREF_VCARD_PHOTO_CMD); + + // this makes it work with filenames that contain spaces + g_string_prepend(filename, "\""); + g_string_append(filename, "\""); + gchar* cmd = str_replace(cmdtemplate, "%p", filename->str); + g_free(cmdtemplate); + + if (g_shell_parse_argv(cmd, &argc, &argv, &err) == FALSE) { + cons_show_error("Failed to parse command template"); + g_free(cmd); + } else { + if (!call_external(argv)) { + cons_show_error("Unable to execute command"); + } + g_strfreev(argv); + } + } + + g_string_free(filename, TRUE); + + return 1; +} + +void +vcard_photo(xmpp_ctx_t* ctx, char* jid, char* filename, int index, gboolean open) +{ + _userdata* data = calloc(1, sizeof(_userdata)); + data->vcard = vcard_new(); + + if (!data || !data->vcard) { + if (data) { + free(data); + } + + cons_show("vCard allocation failed"); + return; + } + + data->photo_index = index; + data->open = open; + + if (filename) { + data->filename = strdup(filename); + } + + char* id = connection_create_stanza_id(); + xmpp_stanza_t* iq = stanza_create_vcard_request_iq(ctx, jid, id); + + iq_id_handler_add(id, _vcard_photo_result, (ProfIqFreeCallback)_free_userdata, data); + + free(id); + iq_send_stanza(iq); + xmpp_stanza_release(iq); +} + +static int +_vcard_refresh_result(xmpp_stanza_t* const stanza, void* userdata) +{ + xmpp_stanza_t* vcard_xml = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_VCARD); + + vcard_free_full(vcard_user); + if (!vcard_parse(vcard_xml, vcard_user)) { + return 1; + } + + cons_show("vCard refreshed"); + vcard_user->modified = FALSE; + return 1; +} + +void +vcard_user_refresh(void) +{ + if (!vcard_user) { + vcard_user = vcard_new(); + } + + if (!vcard_user) { + return; + } + + char* id = connection_create_stanza_id(); + xmpp_stanza_t* iq = stanza_create_vcard_request_iq(connection_get_ctx(), NULL, id); + + iq_id_handler_add(id, _vcard_refresh_result, NULL, NULL); + + free(id); + iq_send_stanza(iq); + xmpp_stanza_release(iq); +} + +// Upload a vCard and set it as the currently connected account's vCard +void +vcard_upload(xmpp_ctx_t* ctx, vCard* vcard) +{ + char* id = connection_create_stanza_id(); + xmpp_stanza_t* iq = xmpp_iq_new(ctx, STANZA_TYPE_SET, id); + xmpp_stanza_set_from(iq, connection_get_fulljid()); + + xmpp_stanza_t* vcard_stanza = vcard_to_xml(ctx, vcard); + + if (!vcard_stanza) { + xmpp_stanza_release(iq); + free(id); + return; + } + + xmpp_stanza_add_child(iq, vcard_stanza); + xmpp_stanza_release(vcard_stanza); + + free(id); + iq_send_stanza(iq); + xmpp_stanza_release(iq); +} + +void +vcard_user_save(void) +{ + vcard_upload(connection_get_ctx(), vcard_user); + vcard_user->modified = FALSE; +} + +void +vcard_user_set_fullname(char* fullname) +{ + if (vcard_user->fullname) { + free(vcard_user->fullname); + } + if (fullname) { + vcard_user->fullname = strdup(fullname); + } else { + vcard_user->fullname = NULL; + } + + vcard_user->modified = TRUE; +} + +void +vcard_user_set_name_family(char* family) +{ + if (vcard_user->name.family) { + free(vcard_user->name.family); + } + if (family) { + vcard_user->name.family = strdup(family); + } else { + vcard_user->name.family = NULL; + } + + vcard_user->modified = TRUE; +} + +void +vcard_user_set_name_given(char* given) +{ + if (vcard_user->name.given) { + free(vcard_user->name.given); + } + if (given) { + vcard_user->name.given = strdup(given); + } else { + vcard_user->name.given = NULL; + } + + vcard_user->modified = TRUE; +} + +void +vcard_user_set_name_middle(char* middle) +{ + if (vcard_user->name.middle) { + free(vcard_user->name.middle); + } + if (middle) { + vcard_user->name.middle = strdup(middle); + } else { + vcard_user->name.middle = NULL; + } + + vcard_user->modified = TRUE; +} + +void +vcard_user_set_name_prefix(char* prefix) +{ + if (vcard_user->name.prefix) { + free(vcard_user->name.prefix); + } + if (prefix) { + vcard_user->name.prefix = strdup(prefix); + } else { + vcard_user->name.prefix = NULL; + } + + vcard_user->modified = TRUE; +} + +void +vcard_user_set_name_suffix(char* suffix) +{ + if (vcard_user->name.suffix) { + free(vcard_user->name.suffix); + } + if (suffix) { + vcard_user->name.suffix = strdup(suffix); + } else { + vcard_user->name.suffix = NULL; + } + + vcard_user->modified = TRUE; +} + +void +vcard_user_add_element(vcard_element_t* element) +{ + g_queue_push_tail(vcard_user->elements, element); + vcard_user->modified = TRUE; +} + +void +vcard_user_remove_element(unsigned int index) +{ + void* pointer = g_queue_pop_nth(vcard_user->elements, index); + + if (pointer) { + _free_vcard_element(pointer); + vcard_user->modified = TRUE; + } +} + +vcard_element_t* +vcard_user_get_element_index(unsigned int index) +{ + return g_queue_peek_nth(vcard_user->elements, index); +} + +ProfWin* +vcard_user_create_win(void) +{ + return wins_new_vcard(vcard_user); +} + +void +vcard_user_free(void) +{ + if (vcard_user) { + vcard_free_full(vcard_user); + vcard_free(vcard_user); + } + vcard_user = NULL; +} diff --git a/src/xmpp/vcard.h b/src/xmpp/vcard.h new file mode 100644 index 00000000..9ec04c8c --- /dev/null +++ b/src/xmpp/vcard.h @@ -0,0 +1,171 @@ +/* + * vcard.h + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org> + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see <https://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#ifndef XMPP_VCARD_H +#define XMPP_VCARD_H + +#include <glib.h> + +// 17 bits are currently used, out of (a possible) 32 bits +typedef enum { + VCARD_HOME = 1, + VCARD_WORK = 2, + VCARD_POSTAL = 4, + VCARD_PARCEL = 8, + VCARD_INTL = 16, + VCARD_PREF = 32, + VCARD_TEL_VOICE = 64, + VCARD_TEL_FAX = 128, + VCARD_TEL_PAGER = 256, + VCARD_TEL_MSG = 512, + VCARD_TEL_CELL = 1024, + VCARD_TEL_VIDEO = 2048, + VCARD_TEL_BBS = 4096, + VCARD_TEL_MODEM = 8192, + VCARD_TEL_ISDN = 16384, + VCARD_TEL_PCS = 32768, + VCARD_EMAIL_X400 = 65536, + VCARD_EMAIL_INTERNET = 131072, + VCARD_DOM = 262144 +} vcard_element_options_t; + +typedef struct _vcard_name +{ + char *family, *given, *middle, *prefix, *suffix; +} vcard_name_t; + +typedef struct _vcard_element_photo +{ + union { + struct + { + guchar* data; + char* type; + gsize length; + }; + char* extval; + }; + gboolean external; +} vcard_element_photo_t; + +typedef struct _vcard_element_address +{ + char *pobox, *extaddr, *street, *locality, *region, *pcode, *country; + + // Options used: + // VCARD_HOME + // VCARD_WORK + // VCARD_POSTAL + // VCARD_PARCEL + // VCARD_DOM + // VCARD_INTL + // VCARD_PREF + vcard_element_options_t options; +} vcard_element_address_t; + +typedef struct _vcard_element_telephone +{ + char* number; + + // Options used: + // VCARD_HOME + // VCARD_WORK + // VCARD_TEL_VOICE + // VCARD_TEL_FAX + // VCARD_TEL_PAGER + // VCARD_TEL_MSG + // VCARD_TEL_CELL + // VCARD_TEL_VIDEO + // VCARD_TEL_BBS + // VCARD_TEL_MODEM + // VCARD_TEL_ISDN + // VCARD_TEL_PCS + // VCARD_PREF + vcard_element_options_t options; +} vcard_element_telephone_t; + +typedef struct _vcard_element_email +{ + char* userid; + + // Options used: + // VCARD_HOME + // VCARD_WORK + // VCARD_EMAIL_X400 + // VCARD_PREF + vcard_element_options_t options; +} vcard_element_email_t; + +typedef enum _vcard_element_type { + VCARD_NICKNAME, + VCARD_PHOTO, + VCARD_BIRTHDAY, + VCARD_ADDRESS, + VCARD_TELEPHONE, + VCARD_EMAIL, + VCARD_JID, + VCARD_TITLE, + VCARD_ROLE, + VCARD_NOTE, + VCARD_URL +} vcard_element_type; + +typedef struct _vcard_element +{ + vcard_element_type type; + + union { + char *nickname, *jid, *title, *role, *note, *url; + vcard_element_photo_t photo; + GDateTime* birthday; + vcard_element_address_t address; + vcard_element_telephone_t telephone; + vcard_element_email_t email; + }; +} vcard_element_t; + +typedef struct _vcard +{ + // These elements are only meant to appear once (per DTD) + vcard_name_t name; + char* fullname; + + gboolean modified; + + // GQueue of vcard_element* + GQueue* elements; +} vCard; + +#endif diff --git a/src/xmpp/vcard_funcs.h b/src/xmpp/vcard_funcs.h new file mode 100644 index 00000000..5f4aa3d0 --- /dev/null +++ b/src/xmpp/vcard_funcs.h @@ -0,0 +1,66 @@ +/* + * vcard_funcs.h + * vim: expandtab:ts=4:sts=4:sw=4 + * + * Copyright (C) 2022 Marouane L. <techmetx11@disroot.org> + * + * This file is part of Profanity. + * + * Profanity is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Profanity is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Profanity. If not, see <https://www.gnu.org/licenses/>. + * + * In addition, as a special exception, the copyright holders give permission to + * link the code of portions of this program with the OpenSSL library under + * certain conditions as described in each individual source file, and + * distribute linked combinations including the two. + * + * You must obey the GNU General Public License in all respects for all of the + * code used other than OpenSSL. If you modify file(s) with this exception, you + * may extend this exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. If you delete this exception statement from all + * source files in the program, then also delete it here. + * + */ + +#ifndef XMPP_VCARD_FUNCS_H +#define XMPP_VCARD_FUNCS_H + +#include "ui/win_types.h" +#include "xmpp/vcard.h" + +vCard* vcard_new(); +void vcard_free(vCard* vcard); +void vcard_free_full(vCard* vcard); + +gboolean vcard_parse(xmpp_stanza_t* vcard_xml, vCard* vcard); + +void vcard_print(xmpp_ctx_t* ctx, ProfWin* window, char* jid); +void vcard_photo(xmpp_ctx_t* ctx, char* jid, char* filename, int index, gboolean open); + +void vcard_user_refresh(void); +void vcard_user_save(void); +void vcard_user_set_fullname(char* fullname); +void vcard_user_set_name_family(char* family); +void vcard_user_set_name_given(char* given); +void vcard_user_set_name_middle(char* middle); +void vcard_user_set_name_prefix(char* prefix); +void vcard_user_set_name_suffix(char* suffix); + +void vcard_user_add_element(vcard_element_t* element); +void vcard_user_remove_element(unsigned int index); +vcard_element_t* vcard_user_get_element_index(unsigned int index); +ProfWin* vcard_user_create_win(); + +void vcard_user_free(void); +#endif diff --git a/tests/unittests/ui/stub_ui.c b/tests/unittests/ui/stub_ui.c index ee759bb2..f95ee0d8 100644 --- a/tests/unittests/ui/stub_ui.c +++ b/tests/unittests/ui/stub_ui.c @@ -1256,6 +1256,11 @@ win_create_plugin(const char* const plugin_name, const char* const tag) { return NULL; } +ProfWin* +win_create_vcard(vCard* vcard) +{ + return NULL; +} char* win_get_tab_identifier(ProfWin* window) diff --git a/tests/unittests/ui/stub_vcardwin.c b/tests/unittests/ui/stub_vcardwin.c new file mode 100644 index 00000000..e73eb04b --- /dev/null +++ b/tests/unittests/ui/stub_vcardwin.c @@ -0,0 +1,4 @@ +void +vcardwin_update(void) +{ +} diff --git a/tests/unittests/xmpp/stub_vcard.c b/tests/unittests/xmpp/stub_vcard.c new file mode 100644 index 00000000..cc48f92c --- /dev/null +++ b/tests/unittests/xmpp/stub_vcard.c @@ -0,0 +1,79 @@ +#include "ui/ui.h" +#include "xmpp/vcard.h" + +ProfWin* +vcard_user_create_win() +{ + return NULL; +} + +void +vcard_user_add_element(vcard_element_t* element) +{ +} + +void +vcard_user_remove_element(unsigned int index) +{ +} + +void +vcard_print(xmpp_ctx_t* ctx, ProfWin* window, char* jid) +{ +} + +void +vcard_photo(xmpp_ctx_t* ctx, char* jid, char* filename, int index, gboolean open) +{ +} + +void +vcard_user_refresh(void) +{ +} + +void +vcard_user_save(void) +{ +} + +void +vcard_user_set_fullname(char* fullname) +{ +} + +void +vcard_user_set_name_family(char* family) +{ +} + +void +vcard_user_set_name_given(char* given) +{ +} + +void +vcard_user_set_name_middle(char* middle) +{ +} + +void +vcard_user_set_name_prefix(char* prefix) +{ +} + +void +vcard_user_set_name_suffix(char* suffix) +{ +} + +vcard_element_t* +vcard_user_get_element_index(unsigned int index) +{ + return NULL; +} + +void +vcard_user_free(void) +{ +}