/* This implements a complete suite for caching and searching contacts in the * Shell. We retrieve contacts from libfolks asynchronously and we search * these for display to the user. */ #include "shell-contact-system.h" #include #include #include #include #include #include "shell-global.h" #include "shell-util.h" #include "st.h" G_DEFINE_TYPE (ShellContactSystem, shell_contact_system, G_TYPE_OBJECT); #define NAME_PREFIX_MATCH_WEIGHT 100 #define NAME_SUBSTRING_MATCH_WEIGHT 90 #define ADDR_PREFIX_MATCH_WEIGHT 10 #define ADDR_SUBSTRING_MATCH_WEIGHT 5 /* Callbacks */ static void prepare_individual_aggregator_cb (GObject *obj, GAsyncResult *res, gpointer user_data) { FolksIndividualAggregator *aggregator = FOLKS_INDIVIDUAL_AGGREGATOR (obj); folks_individual_aggregator_prepare_finish (aggregator, res, NULL); } /* Internal stuff */ typedef struct { gchar *key; guint weight; } ContactSearchResult; struct _ShellContactSystemPrivate { FolksIndividualAggregator *aggregator; }; static void shell_contact_system_constructed (GObject *obj) { ShellContactSystem *self = SHELL_CONTACT_SYSTEM (obj); G_OBJECT_CLASS (shell_contact_system_parent_class)->constructed (obj); /* We intentionally do not care about the "individuals-changed" signal, as * we don't intend to update searches after they've been performed. * Therefore, we will simply retrieve the "individuals" property which * represents a snapshot of the individuals in the aggregator. */ self->priv->aggregator = folks_individual_aggregator_new (); folks_individual_aggregator_prepare (self->priv->aggregator, prepare_individual_aggregator_cb, NULL); } static void shell_contact_system_finalize (GObject *obj) { ShellContactSystem *self = SHELL_CONTACT_SYSTEM (obj); g_object_unref (self->priv->aggregator); G_OBJECT_CLASS (shell_contact_system_parent_class)->finalize (obj); } static void shell_contact_system_init (ShellContactSystem *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, SHELL_TYPE_CONTACT_SYSTEM, ShellContactSystemPrivate); } static void shell_contact_system_class_init (ShellContactSystemClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = shell_contact_system_constructed; object_class->finalize = shell_contact_system_finalize; g_type_class_add_private (object_class, sizeof (ShellContactSystemPrivate)); } /** * normalize_terms: * @terms: (element-type utf8): Input search terms * * Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms */ static GSList * normalize_terms (GSList *terms) { GSList *normalized_terms = NULL; GSList *iter; for (iter = terms; iter; iter = iter->next) { const char *term = iter->data; normalized_terms = g_slist_prepend (normalized_terms, shell_util_normalize_and_casefold (term)); } return normalized_terms; } static guint do_match (ShellContactSystem *self, FolksIndividual *individual, GSList *terms) { GSList *term_iter; guint weight = 0; char *alias = shell_util_normalize_and_casefold (folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual))); char *name = shell_util_normalize_and_casefold (folks_name_details_get_full_name (FOLKS_NAME_DETAILS (individual))); char *nick = shell_util_normalize_and_casefold (folks_name_details_get_nickname (FOLKS_NAME_DETAILS (individual))); GeeMultiMap *im_addr_map = folks_im_details_get_im_addresses (FOLKS_IM_DETAILS (individual)); GeeCollection *im_addrs = gee_multi_map_get_values (im_addr_map); GeeSet *email_addrs = folks_email_details_get_email_addresses (FOLKS_EMAIL_DETAILS (individual)); GeeIterator *addrs_iter; gboolean have_name_prefix = FALSE; gboolean have_name_substring = FALSE; gboolean have_addr_prefix = FALSE; gboolean have_addr_substring = FALSE; for (term_iter = terms; term_iter; term_iter = term_iter->next) { const char *term = term_iter->data; const char *p; gboolean matched; matched = FALSE; /* Match on alias, name, nickname */ if (alias != NULL) { p = strstr (alias, term); if (p == alias) { have_name_prefix = TRUE; matched = TRUE; } else if (p != NULL) { have_name_substring = TRUE; matched = TRUE; } } if (name != NULL) { p = strstr (name, term); if (p == name) { have_name_prefix = TRUE; matched = TRUE; } else if (p != NULL) { have_name_substring = TRUE; matched = TRUE; } } if (nick != NULL) { p = strstr (nick, term); if (p == nick) { have_name_prefix = TRUE; matched = TRUE; } else if (p != NULL) { have_name_substring = TRUE; matched = TRUE; } } /* Match on one or more IM or email addresses */ addrs_iter = gee_iterable_iterator (GEE_ITERABLE (im_addrs)); while (gee_iterator_next (addrs_iter)) { const gchar *addr = gee_iterator_get (addrs_iter); p = strstr (addr, term); if (p == addr) { have_addr_prefix = TRUE; matched = TRUE; } else if (p != NULL) { have_addr_substring = TRUE; matched = TRUE; } } g_object_unref (addrs_iter); addrs_iter = gee_iterable_iterator (GEE_ITERABLE (email_addrs)); while (gee_iterator_next (addrs_iter)) { const gchar *addr = gee_iterator_get (addrs_iter); p = strstr (addr, term); if (p == addr) { have_addr_prefix = TRUE; matched = TRUE; } else if (p != NULL) { have_addr_substring = TRUE; matched = TRUE; } } g_object_unref (addrs_iter); if (!matched) { have_name_prefix = FALSE; have_name_substring = FALSE; have_addr_prefix = FALSE; have_addr_substring = FALSE; break; } } if (have_name_prefix) weight += NAME_PREFIX_MATCH_WEIGHT; else if (have_name_substring) weight += NAME_SUBSTRING_MATCH_WEIGHT; if (have_addr_prefix) weight += ADDR_PREFIX_MATCH_WEIGHT; else if (have_addr_substring) weight += ADDR_SUBSTRING_MATCH_WEIGHT; g_free (alias); g_free (name); g_free (nick); g_object_unref (im_addrs); return weight; } static gint compare_results (gconstpointer a, gconstpointer b) { ContactSearchResult *first = (ContactSearchResult *) a; ContactSearchResult *second = (ContactSearchResult *) b; if (first->weight > second->weight) return 1; else if (first->weight < second->weight) return -1; else return 0; } static void free_result (gpointer data, gpointer user_data) { g_slice_free (ContactSearchResult, data); } /* modifies and frees @results */ static GSList * sort_and_prepare_results (GSList *results) { GSList *iter; GSList *sorted_results = NULL; results = g_slist_sort (results, compare_results); for (iter = results; iter; iter = iter->next) { ContactSearchResult *result = iter->data; gchar *id = result->key; sorted_results = g_slist_prepend (sorted_results, id); } g_slist_foreach (results, (GFunc) free_result, NULL); return sorted_results; } /* Methods */ /** * shell_contact_system_get_default: * * Return Value: (transfer none): The global #ShellContactSystem singleton */ ShellContactSystem * shell_contact_system_get_default (void) { static ShellContactSystem *instance = NULL; if (instance == NULL) instance = g_object_new (SHELL_TYPE_CONTACT_SYSTEM, NULL); return instance; } /** * shell_contact_system_get_all: * @self: A #ShellContactSystem * * Returns: (transfer none): All individuals */ GeeMap * shell_contact_system_get_all (ShellContactSystem *self) { GeeMap *individuals; g_return_val_if_fail (SHELL_IS_CONTACT_SYSTEM (self), NULL); individuals = folks_individual_aggregator_get_individuals (self->priv->aggregator); return individuals; } /** * shell_contact_system_get_individual: * @self: A #ShellContactSystem * @id: A #gchar with the ID of the FolksIndividual to be returned. * * Returns: (transfer full): A #FolksIndividual or NULL if @id could not be found. */ FolksIndividual * shell_contact_system_get_individual (ShellContactSystem *self, gchar *id) { GeeMap *individuals; gpointer key, value; key = (gpointer) id; g_return_val_if_fail (SHELL_IS_CONTACT_SYSTEM (self), NULL); individuals = folks_individual_aggregator_get_individuals (self->priv->aggregator); value = gee_map_get (individuals, key); return FOLKS_INDIVIDUAL (value); } /** * shell_contact_system_get_email_for_display: * @self: A #ShellContactSystem * @individual A #FolksIndividual * * Get an email address (either from IM addresses or email), which can be * used to represent @individual. * * Return: (transfer full): a newly allocated string or %NULL if no address * was found */ char * shell_contact_system_get_email_for_display (ShellContactSystem *self, FolksIndividual *individual) { GeeMultiMap *im_addr_map = folks_im_details_get_im_addresses (FOLKS_IM_DETAILS (individual)); GeeCollection *im_addrs = gee_multi_map_get_values (im_addr_map); GeeSet *email_addrs = folks_email_details_get_email_addresses (FOLKS_EMAIL_DETAILS (individual)); GeeIterator *addrs_iter; char *email = NULL; addrs_iter = gee_iterable_iterator (GEE_ITERABLE (im_addrs)); if (gee_iterator_first (addrs_iter)) { FolksImFieldDetails *field = gee_iterator_get (addrs_iter); email = g_strdup (folks_abstract_field_details_get_value ((FolksAbstractFieldDetails*)field)); g_object_unref (field); } g_object_unref (addrs_iter); g_object_unref (im_addrs); if (email != NULL) return email; addrs_iter = gee_iterable_iterator (GEE_ITERABLE (email_addrs)); if (gee_iterator_first (addrs_iter)) { FolksEmailFieldDetails *field = gee_iterator_get (addrs_iter); email = g_strdup (folks_abstract_field_details_get_value ((FolksAbstractFieldDetails*)field)); g_object_unref (field); } g_object_unref (addrs_iter); return email; } /** * shell_contact_system_initial_search: * @shell: A #ShellContactSystem * @terms: (element-type utf8): List of terms, logical AND * * Search through contacts for the given search terms. * * Returns: (transfer container) (element-type utf8): List of contact * identifiers */ GSList * shell_contact_system_initial_search (ShellContactSystem *self, GSList *terms) { FolksIndividual *individual; GSList *results = NULL; GeeMap *individuals = NULL; ContactSearchResult *result; GeeMapIterator *iter; gpointer key; guint weight; GSList *normalized_terms = normalize_terms (terms); g_return_val_if_fail (SHELL_IS_CONTACT_SYSTEM (self), NULL); individuals = folks_individual_aggregator_get_individuals (self->priv->aggregator); iter = gee_map_map_iterator (individuals); while (gee_map_iterator_next (iter)) { individual = gee_map_iterator_get_value (iter); weight = do_match (self, individual, normalized_terms); if (weight != 0) { key = gee_map_iterator_get_key (iter); result = g_slice_new (ContactSearchResult); result->key = (gchar *) key; result->weight = weight; results = g_slist_append (results, result); } g_object_unref (individual); } return sort_and_prepare_results (results); } /** * shell_contact_system_subsearch: * @shell: A #ShellContactSystem * @previous_results: (element-type utf8): List of previous results * @terms: (element-type utf8): List of terms, logical AND * * Search through a previous result set; for more information see * js/ui/search.js. * * Returns: (transfer container) (element-type utf8): List of contact * identifiers */ GSList * shell_contact_system_subsearch (ShellContactSystem *self, GSList *previous_results, GSList *terms) { return shell_contact_system_initial_search (self, terms); }