tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PersonalityProvider.sys.mjs (7511B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 const lazy = {};
      6 
      7 ChromeUtils.defineESModuleGetters(lazy, {
      8  BasePromiseWorker: "resource://gre/modules/PromiseWorker.sys.mjs",
      9  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
     10  RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
     11  Utils: "resource://services-settings/Utils.sys.mjs",
     12 });
     13 
     14 const RECIPE_NAME = "personality-provider-recipe";
     15 const MODELS_NAME = "personality-provider-models";
     16 
     17 export class PersonalityProvider {
     18  constructor(modelKeys) {
     19    this.modelKeys = modelKeys;
     20    this.onSync = this.onSync.bind(this);
     21    this.setup();
     22  }
     23 
     24  setScores(scores) {
     25    this.scores = scores || {};
     26    this.interestConfig = this.scores.interestConfig;
     27    this.interestVector = this.scores.interestVector;
     28  }
     29 
     30  get personalityProviderWorker() {
     31    if (this._personalityProviderWorker) {
     32      return this._personalityProviderWorker;
     33    }
     34 
     35    this._personalityProviderWorker = new lazy.BasePromiseWorker(
     36      "resource://newtab/lib/PersonalityProvider/PersonalityProvider.worker.mjs",
     37      { type: "module" }
     38    );
     39 
     40    return this._personalityProviderWorker;
     41  }
     42 
     43  setup() {
     44    this.setupSyncAttachment(RECIPE_NAME);
     45    this.setupSyncAttachment(MODELS_NAME);
     46  }
     47 
     48  teardown() {
     49    this.teardownSyncAttachment(RECIPE_NAME);
     50    this.teardownSyncAttachment(MODELS_NAME);
     51    if (this._personalityProviderWorker) {
     52      this._personalityProviderWorker.terminate();
     53    }
     54  }
     55 
     56  setupSyncAttachment(collection) {
     57    lazy.RemoteSettings(collection).on("sync", this.onSync);
     58  }
     59 
     60  teardownSyncAttachment(collection) {
     61    lazy.RemoteSettings(collection).off("sync", this.onSync);
     62  }
     63 
     64  onSync(event) {
     65    this.personalityProviderWorker.post("onSync", [event]);
     66  }
     67 
     68  /**
     69   * Gets contents of the attachment if it already exists on file,
     70   * and if not attempts to download it.
     71   */
     72  getAttachment(record) {
     73    return this.personalityProviderWorker.post("getAttachment", [record]);
     74  }
     75 
     76  /**
     77   * Returns a Recipe from remote settings to be consumed by a RecipeExecutor.
     78   * A Recipe is a set of instructions on how to processes a RecipeExecutor.
     79   */
     80  async getRecipe() {
     81    if (!this.recipes || !this.recipes.length) {
     82      const result = await lazy.RemoteSettings(RECIPE_NAME).get();
     83      this.recipes = await Promise.all(
     84        result.map(async record => ({
     85          ...(await this.getAttachment(record)),
     86          recordKey: record.key,
     87        }))
     88      );
     89    }
     90    return this.recipes[0];
     91  }
     92 
     93  /**
     94   * Grabs a slice of browse history for building a interest vector
     95   */
     96  async fetchHistory(columns, beginTimeSecs, endTimeSecs) {
     97    let sql = `SELECT url, title, visit_count, frecency, last_visit_date, description
     98    FROM moz_places
     99    WHERE last_visit_date >= ${beginTimeSecs * 1000000}
    100    AND last_visit_date < ${endTimeSecs * 1000000}`;
    101    columns.forEach(requiredColumn => {
    102      sql += ` AND IFNULL(${requiredColumn}, '') <> ''`;
    103    });
    104    sql += " LIMIT 30000";
    105 
    106    const { activityStreamProvider } = lazy.NewTabUtils;
    107    const history = await activityStreamProvider.executePlacesQuery(sql, {
    108      columns,
    109      params: {},
    110    });
    111 
    112    return history;
    113  }
    114 
    115  /**
    116   * Handles setup and metrics of history fetch.
    117   */
    118  async getHistory() {
    119    let endTimeSecs = new Date().getTime() / 1000;
    120    let beginTimeSecs = endTimeSecs - this.interestConfig.history_limit_secs;
    121    if (
    122      !this.interestConfig ||
    123      !this.interestConfig.history_required_fields ||
    124      !this.interestConfig.history_required_fields.length
    125    ) {
    126      return [];
    127    }
    128    let history = await this.fetchHistory(
    129      this.interestConfig.history_required_fields,
    130      beginTimeSecs,
    131      endTimeSecs
    132    );
    133 
    134    return history;
    135  }
    136 
    137  async setBaseAttachmentsURL() {
    138    await this.personalityProviderWorker.post("setBaseAttachmentsURL", [
    139      await lazy.Utils.baseAttachmentsURL(),
    140    ]);
    141  }
    142 
    143  async setInterestConfig() {
    144    this.interestConfig = this.interestConfig || (await this.getRecipe());
    145    await this.personalityProviderWorker.post("setInterestConfig", [
    146      this.interestConfig,
    147    ]);
    148  }
    149 
    150  async setInterestVector() {
    151    await this.personalityProviderWorker.post("setInterestVector", [
    152      this.interestVector,
    153    ]);
    154  }
    155 
    156  async fetchModels() {
    157    const models = await lazy.RemoteSettings(MODELS_NAME).get();
    158    return this.personalityProviderWorker.post("fetchModels", [models]);
    159  }
    160 
    161  async generateTaggers() {
    162    await this.personalityProviderWorker.post("generateTaggers", [
    163      this.modelKeys,
    164    ]);
    165  }
    166 
    167  async generateRecipeExecutor() {
    168    await this.personalityProviderWorker.post("generateRecipeExecutor");
    169  }
    170 
    171  async createInterestVector() {
    172    const history = await this.getHistory();
    173 
    174    const interestVectorResult = await this.personalityProviderWorker.post(
    175      "createInterestVector",
    176      [history]
    177    );
    178 
    179    return interestVectorResult;
    180  }
    181 
    182  async init(callback) {
    183    await this.setBaseAttachmentsURL();
    184    await this.setInterestConfig();
    185    if (!this.interestConfig) {
    186      return;
    187    }
    188 
    189    // We always generate a recipe executor, no cache used here.
    190    // This is because the result of this is an object with
    191    // functions (taggers) so storing it in cache is not possible.
    192    // Thus we cannot use it to rehydrate anything.
    193    const fetchModelsResult = await this.fetchModels();
    194    // If this fails, log an error and return.
    195    if (!fetchModelsResult.ok) {
    196      return;
    197    }
    198    await this.generateTaggers();
    199    await this.generateRecipeExecutor();
    200 
    201    // If we don't have a cached vector, create a new one.
    202    if (!this.interestVector) {
    203      const interestVectorResult = await this.createInterestVector();
    204      // If that failed, log an error and return.
    205      if (!interestVectorResult.ok) {
    206        return;
    207      }
    208      this.interestVector = interestVectorResult.interestVector;
    209    }
    210 
    211    // This happens outside the createInterestVector call above,
    212    // because create can be skipped if rehydrating from cache.
    213    // In that case, the interest vector is provided and not created, so we just set it.
    214    await this.setInterestVector();
    215 
    216    this.initialized = true;
    217    if (callback) {
    218      callback();
    219    }
    220  }
    221 
    222  async calculateItemRelevanceScore(pocketItem) {
    223    if (!this.initialized) {
    224      return pocketItem.item_score || 1;
    225    }
    226    const itemRelevanceScore = await this.personalityProviderWorker.post(
    227      "calculateItemRelevanceScore",
    228      [pocketItem]
    229    );
    230    if (!itemRelevanceScore) {
    231      return -1;
    232    }
    233    const { scorableItem, rankingVector } = itemRelevanceScore;
    234    // Put the results on the item for debugging purposes.
    235    pocketItem.scorableItem = scorableItem;
    236    pocketItem.rankingVector = rankingVector;
    237    return rankingVector.score;
    238  }
    239 
    240  /**
    241   * Returns an object holding the personalization scores of this provider instance.
    242   */
    243  getScores() {
    244    return {
    245      // We cannot return taggers here.
    246      // What we return here goes into persistent cache, and taggers have functions on it.
    247      // If we attempted to save taggers into persistent cache, it would store it to disk,
    248      // and the next time we load it, it would start thowing function is not defined.
    249      interestConfig: this.interestConfig,
    250      interestVector: this.interestVector,
    251    };
    252  }
    253 }