Реклама в Ubuntu 12.10 Взгляд изнутри

Много шума от так называемого нововведения в Ubuntu. Многие считают, что показ рекомендуемых товаров в отдельной области оскорбителен, и является веским поводом для смены дистрибутива.
Вот, что скажите тут такого...

http://ubuntovod.ru/wp-content/uploads/2012/10/v-ubuntu-poyavilas-reklama-polzovateli-vozmushheny.jpg

Нижней строкой идут рекомендуемые товары, точно также у меня показывается видио с Youtube, приложения c AppStore Ubuntu-software-center и т.п.
Решил я покапаться в исходниках этой линзы и нашел , насколько я понял, не слишком приятный момент. Как мне показалось, просмотрев код

/*
 * Copyright (C) 2012 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authored by Michal Hruby <michal.hruby@canonical.com>
 *
 */
 
using GLib;
 
namespace Unity.ShoppingLens
{
  public class ShoppingScope : Unity.Scope
  {
    private const string OFFERS_BASE_URI = "http://productsearch.ubuntu.com";
 
    private HashTable<string, string> results_details_map;
    private HashTable<string, string> global_results_details_map;
 
    public ShoppingScope ()
    {
      Object (dbus_path: "/com/canonical/unity/scope/shopping");
    }
 
    protected override void constructed ()
    {
      /* Listen for filter changes */
      filters_changed.connect (() => {
        queue_search_changed (SearchType.DEFAULT);
      });
 
      active_sources_changed.connect (() => {
        queue_search_changed (SearchType.DEFAULT);
      });
 
      /* No need to search if only the whitespace changes */
      generate_search_key.connect ((lens_search) => {
        return lens_search.search_string.strip ();
      });
 
      /* Listen for changes to the lens search entry */
      search_changed.connect ((search, search_type, cancellable) => {
        update_search_async.begin (search, search_type, cancellable);
      });
 
      preview_uri.connect ((uri) => {
        // FIXME: async?!
        return generate_preview_for_uri (uri);
      });
 
      // FIXME: the models will be re-created if this will be a remote scope
      results_details_map = new HashTable<string, string> (str_hash, str_equal);
      results_model.set_data ("details-map", results_details_map);
      global_results_details_map = new HashTable<string, string> (str_hash,
                                                                  str_equal);
      global_results_model.set_data ("details-map", global_results_details_map);
    }
 
    private async void update_search_async (LensSearch search,
                                            SearchType search_type,
                                            Cancellable cancellable)
    {
      if (search_type == SearchType.GLOBAL)
        yield perform_global_search (search, cancellable);
      else
        yield perform_search (search, cancellable);
 
      search.finished ();
    }
 
    private async void perform_global_search (LensSearch search,
                                              Cancellable cancellable)
    {
      search.results_model.clear ();
      string query_string = search.search_string.strip ();
      if (query_string == "") return;
 
      var uri = build_search_uri (query_string, SearchType.GLOBAL);
      try
      {
        var parser = yield get_json_reply_async (uri, cancellable);
        process_search_reply_json (parser, Category.TREAT_YOURSELF,
                                   search.results_model);
      }
      catch (Error err)
      {
        warning ("Error: %s", err.message);
      }
    }
 
    private async void perform_search (LensSearch search,
                                       Cancellable cancellable)
    {
      search.results_model.clear ();
      string query_string = search.search_string.strip ();
      if (query_string == "") return;
 
      // we want prefix matching too
      if (!query_string.has_suffix ("*")) query_string += "*";
      var uri = build_search_uri (query_string, SearchType.DEFAULT);
      try
      {
        var parser = yield get_json_reply_async (uri, cancellable);
        process_search_reply_json (parser, Category.PURCHASE,
                                   search.results_model);
      }
      catch (Error err)
      {
        warning ("Error: %s", err.message);
      }
    }
 
    private Preview? generate_preview_for_uri (string uri)
    {
      string? details_uri =
          global_results_details_map[uri] ?? results_details_map[uri];
 
      if (details_uri == null)
      {
        return new GenericPreview (Path.get_basename (uri),
                                   "No data available", null);
      }
 
      try
      {
        var parser = get_json_reply (details_uri, null);
 
        var root = parser.get_root ().get_object ();
        unowned string title = root.get_string_member ("title");
        unowned string description = root.get_string_member ("description_html");
        unowned string price = root.get_string_member ("formatted_price");
        if (price == null) price = root.get_string_member ("price");
 
        var img_obj = root.get_object_member ("images");
        string image_uri = extract_image_uri (img_obj, int.MAX);
 
        Icon? image = null;
        if (image_uri != "")
        {
          image = new FileIcon (File.new_for_uri (image_uri));
        }
 
        var preview = new GenericPreview (title, MarkupCleaner.html_to_pango_markup (description), image);
        var icon_dir = File.new_for_path (ICON_PATH);
        var icon = new FileIcon (icon_dir.get_child ("service-amazon.svg"));
        var buy_action = new PreviewAction ("buy", _("Buy"), icon);
        if (price != null) buy_action.extra_text = price;
        /* Leaving the activation on unity for now */
        // buy_action.activated.connect ((uri) => { });
        preview.add_action (buy_action);
        return preview;
      }
      catch (Error err)
      {
        return new GenericPreview (Path.get_basename (uri), err.message, null);
      }
    }
 
    private string build_search_uri (string query, SearchType search_type)
    {
      StringBuilder s = new StringBuilder ();
 
      unowned string base_uri = Environment.get_variable ("OFFERS_URI");
      if (base_uri == null) base_uri = OFFERS_BASE_URI;
 
      s.append (base_uri);
      s.append ("/v1/search?q=");
      s.append (Uri.escape_string (query, "", false));
 
      //if (search_type == SearchType.GLOBAL)
      //  s.append ("&include_only_high_relevance=true");
 
      return s.str;
    }
 
    private async Json.Parser get_json_reply_async (string uri,
                                                    Cancellable cancellable)
      throws Error
    {
      message ("Sending request: %s", uri);
 
      var file = File.new_for_uri (uri);
      var stream = yield file.read_async (Priority.DEFAULT, cancellable);
      var parser = new Json.Parser ();
      yield parser.load_from_stream_async (stream);
 
      return parser;
    }
 
    private Json.Parser get_json_reply (string uri,
                                        Cancellable? cancellable)
      throws Error
    {
      message ("Sending sync request: %s", uri);
 
      var file = File.new_for_uri (uri);
      var stream = file.read (cancellable);
      var parser = new Json.Parser ();
      parser.load_from_stream (stream);
 
      return parser;
    }
 
    private HashTable<string, string> get_image_uri_dict (Json.Object image_obj)
    {
      var dict = new HashTable<string, string> (str_hash, str_equal);
 
      foreach (unowned string dimensions in image_obj.get_members ())
      {
        int width, height;
        int res = dimensions.scanf ("%dx%d", out width, out height);
        if (res != 2) continue;
        dict[dimensions] =
          image_obj.get_array_member (dimensions).get_string_element (0);
      }
 
      return dict;
    }
 
    private List<unowned string> get_sorted_keys_for_dim_dict (HashTable<string, string> dict)
    {
      var list = dict.get_keys ();
      list.sort ((a_str, b_str) =>
      {
        int width1, height1, width2, height2;
 
        a_str.scanf ("%dx%d", out width1, out height1);
        b_str.scanf ("%dx%d", out width2, out height2);
 
        return width1 * height1 - width2 * height2;
      });
 
      return list;
    }
 
    /**
     * extract_image_uri:
     *
     * Returns image uri with pixel size (width * height) that is more than
     * or equal to the given pixel size.
     * In case only images with smaller pixel size are available, returns
     * the largest of those.
     */
    private string extract_image_uri (Json.Object image_obj,
                                      int pixel_size)
    {
      var dict = get_image_uri_dict (image_obj);
      var keys_list = get_sorted_keys_for_dim_dict (dict);
      if (keys_list == null) return "";
 
      // short-circuit evaluation
      if (pixel_size == int.MAX) return dict[keys_list.last ().data];
 
      foreach (unowned string dim_string in keys_list)
      {
        int width, height;
        dim_string.scanf ("%dx%d", out width, out height);
        if (width * height >= pixel_size)
        {
          return dict[dim_string];
        }
      }
 
      return dict[keys_list.last ().data];
    }
 
    private void process_search_reply_json (Json.Parser parser,
                                            Category results_category,
                                            Dee.Model results_model)
    {
      HashTable<string, string> details_map =
          results_model.get_data ("details-map");
      details_map.remove_all ();
 
      var root_object = parser.get_root ().get_object ();
      foreach (var r in root_object.get_array_member ("results").get_elements ())
      {
        var result = r.get_object ();
 
        unowned string result_uri = result.get_string_member ("web_purchase_url");
        if (result_uri == null || result_uri == "") continue;
 
        var image_obj = result.get_object_member ("images");
        string image_uri = extract_image_uri (image_obj, 128*128);
 
        unowned string price = result.get_string_member ("formatted_price");
        if (price == null) price = result.get_string_member ("price");
        if (image_uri != "")
        {
          // TODO: what to do if we have price but no icon?
          var file = File.new_for_uri (image_uri);
          var icon = new AnnotatedIcon (new FileIcon (file));
          // FIXME: dash doesn't like empty string as ribbon
          icon.ribbon = price == null || price == "" ? " " : price; 
          // FIXME: the service doesn't expose categories yet
          // icon.category = CategoryType.BOOK;
          image_uri = icon.to_string ();
        }
 
        /* keep the details uri to be able to generate the preview */
        details_map[result_uri] = result.get_string_member ("details");
 
        results_model.append (result_uri,
                              image_uri,
                              results_category,
                              "text/html",
                              result.get_string_member ("title"),
                              "",
                              result_uri);
      }
    }
  }
}

все поисковые запросы отсылаются не в Amazone а на сайт каноникал
http://productsearch.ubuntu.com/
содержащий такой вот код

{
    "Ubuntu Product Search API": {
        "description": "The product search API is used to search
                        product catalogues to get lists of items
                        for purchase. It is primarily used by
                        the product lens in Ubuntu itself.",
        "endpoints": {
            "/v1/search": {
                "parameters": {
                    "q": "The actual search query.",
                    "imagesize": "Size of linked cover art images in search results. 
                                  Defaults to 50. Chosen from list 33, 50, 52, 75, 100, 
                                  175, 180, 182, 200, 350, 500, 800. Some art may not 
                                  exist at high image sizes like 800.",
                    "genres": "Comma-separated list of genres (e.g., “rock,pop”).",
                    "grouping": "'1', to convert list of results into a dictionary
                                 of specific record types, keyed by kind of record.  
                                 Without grouping, [ artist alice, artist bob, album one ].
                                 With grouping, { artist: [alice, bob], album: [ one ] }.
                                 pagesize param and 'total' results are both per-type. ",
                    "page": "Page of results, for pagination. Defaults to 1.",
                    "pagesize": "Number of results returned per page. Defaults to 10.",
                    "decade": "Comma-separated list of decades to filter on for 
                               release date. Style is, e.g., “1980,1990”; allowed 
                               values are 1900-2020.",
                    "geo_store": "A two-letter country code designating the store to 
                                  search. Defaults to the appropriate country for the 
                                  requesting IP. Note that setting this will likely not 
                                  do what you want; if you get results for a store 
                                  inappropriate to your country, then you won't be 
                                  able to buy tracks from that store anyway, so you 
                                  likely shouldn't use this."
                },
                "example_output": {
                    "total": 19,
                    "pagesize": 10,
                    "page": 1
                    "results": [
                        {
                            "purchase_url": "u1ms://…/#url varies by source and geo store",
                            "artist": "The Rolling Stones",
                            "image": "http://cdn.7static.com/static/img/sleeveart/00/008/134/0000813486_50.jpg",
                            "title": "Exile On Main Street",
                            "source": "Ubuntu One Product Store",
                            "year": "2010",
                            "type": "album",
                            "web_purchase_url": ""
                        },
                        …
                    ]
                }
            }
        }
    }
}

и, полагаю, заменить рекламу Amazone на Ozone или Яндексмаркет, или на любой другой ресурс, не получится. Увы :(

Ваша оценка: Нет Средняя оценка: 4 (6 votes)
11
pomodor

Они и не скрывают, что реклама показывается не на прямую, а через сервера Каноникла. Якобы для того, чтобы анонимизировать запросы перед отправкой в Амазон. На самом деле получается, что данные будет собирать и поставщик рекламы, и третья сторона. Причем, у Каноникла данные будут накапливаться в деанонимированном виде. Редиски, в общем.

Ваша оценка: Нет
8

>> Они и не скрывают, что реклама показывается не на прямую, а через сервера Каноникла. Якобы для того, чтобы анонимизировать запросы перед отправкой в Амазон.

Не якобы, а именно для этого. Отдавать тысячи поисковых запросов напрямую в Amazon (а ещё другие поисковики в будущем) -- это была бы уже не "реклама", а предательство.

>> Причем, у Каноникла данные будут накапливаться в деанонимированном виде. Редиски, в общем.

Я что-то в описании протокола (второй блок кода) вообще не вижу каких-либо идентифицирующих пользователя данных, кроме указания страны. Так что данные отправляются скорее всего действительно анонимно.

Ваша оценка: Нет
11
pomodor

Не якобы, а именно для этого.

Именно якобы. Истинная цель — самим собирать базу поисковых запросов. Потом на этой базе можно озолотиться, втюхивая уже таргетированную рекламу. Может им даже Амазон понадобился только для того, чтобы оправдать прикарманивание локальных поисковых запросов.

Ваша оценка: Нет
8

Тогда почему в запросах, отправляемых канониклу, нет никаких идентифицирующих данных?

И да, надо перестать думать, будто все корпорации по определению злые.

Ваша оценка: Нет
9
comrade

Вообще-то так думать вернее
((-;

Ваша оценка: Нет
11
pomodor

надо перестать думать, будто все корпорации по определению злые

Кому именно не надо так думать? Вам? Не думайте! :)

Во-вторых, корпорации не несут зло просто из озорства. У них вообще нет цели творить добро или зло. Задача корпорации — зарабатывать. А наша задача заключается в том, чтобы не давать корпорациям переступать разумные рамки в этом желании заработать. Появление в Линуксе рекламы — это как раз пример такого выхода за рамки.

Тогда почему в запросах, отправляемых канониклу, нет никаких идентифицирующих данных?

А зачем они нужны? Уже по одному IP можно сортировать запросы и потом таргетировать рекламу. Зачем Канониклу знать, что юзера, например, зовут Вася, а не Петя? Достаточно знать, что именно юзер с IP aaa.bbb.ccc.ddd регулярно ищет на своем компе, чтобы определить круг его интересов и подсунуть соответствующую интересам рекламу.

Ваша оценка: Нет
a

Да ладно, обычная партнерская программа с целью монетизации разработки. Вполне привычное явление на винде. Сервера каноникал там нужны только для определения объема траффика с целью уточнения размера выплат.

Ваша оценка: Нет Средняя оценка: 1 (1 vote)
a

Интерфейс Юнити уже начал сильно раздражать. Несколько месяцев на нем проработал, но уже как-то приторно. Раздражает медлительность, боковая вертикальная панель, которую нельзя удалить (можно спрятать, но эта функция реализована очень коряво). То есть, Юнити уже надоел. Хотя такого ощущения никогда не возникало ни с Гномом, ни с XFCE, на с LXDE.

Ваша оценка: Нет Средняя оценка: 5 (1 vote)
7

и дико не хватает возможностей старого Gnome, приятно было переносить панели на любую сторону экрана, менять их размер, добавлять виджеты, да и панель управления(сори) параметры системы, были куда побогаче.

Ваша оценка: Нет
Отправить комментарий
КАПЧА
Вы человек? Подсказка: зарегистрируйтесь, чтобы этот вопрос больше никогда не возникал. Кстати, анонимные ссылки запрещены.
CAPTCHA на основе изображений
Enter the characters shown in the image.
Linux I класса
Linux II класса
Linux III класса
Счетчики
  • Самый популярный сайт о Linux и Windows 10
О Либератуме

Liberatum — это новости мира дистрибутивов Linux, обзоры, сборки, блоги, а также лучший сайт об Ubuntu*.