зеркало из https://github.com/mozilla/snowl.git
713 строки
26 KiB
JavaScript
713 строки
26 KiB
JavaScript
/* ***** BEGIN LICENSE BLOCK *****
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
*
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
* http://www.mozilla.org/MPL/
|
|
*
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
* for the specific language governing rights and limitations under the
|
|
* License.
|
|
*
|
|
* The Original Code is Snowl.
|
|
*
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
* Portions created by the Initial Developer are Copyright (C) 2008
|
|
* the Initial Developer. All Rights Reserved.
|
|
*
|
|
* Contributor(s):
|
|
* Myk Melez <myk@mozilla.org>
|
|
* alta88 <alta88@gmail.com>
|
|
*
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
* the provisions above, a recipient may use your version of this file under
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
*
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
const EXPORTED_SYMBOLS = ["SnowlService"];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
const Cu = Components.utils;
|
|
|
|
// modules that come with Firefox
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
// modules that are generic
|
|
Cu.import("resource://snowl/modules/log4moz.js");
|
|
Cu.import("resource://snowl/modules/Mixins.js");
|
|
Cu.import("resource://snowl/modules/Observers.js");
|
|
Cu.import("resource://snowl/modules/Preferences.js");
|
|
Cu.import("resource://snowl/modules/URI.js");
|
|
|
|
// modules that are Snowl-specific
|
|
Cu.import("resource://snowl/modules/constants.js");
|
|
Cu.import("resource://snowl/modules/datastore.js");
|
|
Cu.import("resource://snowl/modules/message.js");
|
|
Cu.import("resource://snowl/modules/source.js");
|
|
Cu.import("resource://snowl/modules/target.js");
|
|
Cu.import("resource://snowl/modules/utils.js");
|
|
|
|
const PERMS_FILE = 0644;
|
|
const PERMS_DIRECTORY = 0755;
|
|
|
|
const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
|
|
const SNOWL_HANDLER_URI = "chrome://snowl/content/subscribe.xul?feed=%s";
|
|
const SNOWL_HANDLER_TITLE = "Snowl";
|
|
|
|
// How often to check if sources need refreshing, in milliseconds.
|
|
const REFRESH_CHECK_INTERVAL = 60 * 1000; // 60 seconds
|
|
// How often to check message retention policies, in milliseconds.
|
|
// TODO: retention check run based on last run.
|
|
const RETENTION_CHECK_INTERVAL = 60 * 1000 * 60 * 6; // 6 hours
|
|
//const RETENTION_CHECK_INTERVAL = 60 * 1000 * 4; // 4 minutes
|
|
|
|
let SnowlService = {
|
|
get gBrowserWindow() {
|
|
let wm = Cc["@mozilla.org/appshell/window-mediator;1"].
|
|
getService(Ci.nsIWindowMediator);
|
|
delete this._gBrowserWindow;
|
|
return this._gBrowserWindow = wm.getMostRecentWindow("navigator:browser");
|
|
},
|
|
|
|
get _prefs() {
|
|
delete this._prefs;
|
|
return this._prefs = new Preferences("extensions.snowl.");
|
|
},
|
|
|
|
get _dirSvc() {
|
|
let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
|
|
getService(Ci.nsIProperties);
|
|
this.__defineGetter__("_dirSvc", function() { return dirSvc });
|
|
return this._dirSvc;
|
|
},
|
|
|
|
get _converterSvc() {
|
|
let converterSvc =
|
|
Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
|
|
getService(Ci.nsIWebContentConverterService);
|
|
this.__defineGetter__("_converterSvc", function() { return converterSvc });
|
|
return this._converterSvc;
|
|
},
|
|
|
|
get _promptSvc() {
|
|
let promptSvc =
|
|
Cc["@mozilla.org/embedcomp/prompt-service;1"].
|
|
getService(Ci.nsIPromptService);
|
|
this.__defineGetter__("_promptSvc", function() { return promptSvc });
|
|
return this._promptSvc;
|
|
},
|
|
|
|
get _ssSvc() {
|
|
let ssSvc =
|
|
Cc["@mozilla.org/browser/sessionstore;1"].
|
|
getService(Ci.nsISessionStore);
|
|
this.__defineGetter__("_ssSvc", function() { return ssSvc });
|
|
return this._ssSvc;
|
|
},
|
|
|
|
_log: null,
|
|
|
|
_init: function() {
|
|
this._initLogging();
|
|
this._registerFeedHandler();
|
|
this._initRefreshTimer();
|
|
this._initRetentionTimer();
|
|
|
|
Observers.add("snowl:source:added", this.onSourcesChanged, this);
|
|
Observers.add("snowl:source:unstored", this.onSourcesChanged, this);
|
|
},
|
|
|
|
_refreshTimer: null,
|
|
_initRefreshTimer: function() {
|
|
this._refreshTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
let callback = {
|
|
_svc: this,
|
|
notify: function(aTimer) { this._svc.refreshStaleSources() }
|
|
};
|
|
this._refreshTimer.initWithCallback(callback,
|
|
REFRESH_CHECK_INTERVAL,
|
|
Ci.nsITimer.TYPE_REPEATING_SLACK);
|
|
},
|
|
|
|
_retentionTimer: null,
|
|
_initRetentionTimer: function() {
|
|
this._retentionTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
let callback = {
|
|
_svc: this,
|
|
notify: function(aTimer) { this._svc.retentionCheck() }
|
|
};
|
|
this._retentionTimer.initWithCallback(callback,
|
|
RETENTION_CHECK_INTERVAL,
|
|
Ci.nsITimer.TYPE_REPEATING_SLACK);
|
|
},
|
|
|
|
_initLogging: function() {
|
|
let root = Log4Moz.repository.rootLogger;
|
|
root.level = Log4Moz.Level[this._prefs.get("log.logger.root.level")];
|
|
|
|
let formatter = new Log4Moz.BasicFormatter();
|
|
|
|
let capp = new Log4Moz.ConsoleAppender(formatter);
|
|
capp.level = Log4Moz.Level[this._prefs.get("log.appender.console.level")];
|
|
root.addAppender(capp);
|
|
|
|
let dapp = new Log4Moz.DumpAppender(formatter);
|
|
dapp.level = Log4Moz.Level[this._prefs.get("log.appender.dump.level")];
|
|
root.addAppender(dapp);
|
|
|
|
let logFile = this._dirSvc.get("ProfD", Ci.nsIFile);
|
|
logFile.QueryInterface(Ci.nsILocalFile);
|
|
logFile.append("snowl");
|
|
logFile.append("log.txt");
|
|
if (!logFile.exists())
|
|
logFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
|
|
|
|
let fapp = new Log4Moz.RotatingFileAppender(logFile, formatter);
|
|
fapp.level = Log4Moz.Level[this._prefs.get("log.appender.file.level")];
|
|
root.addAppender(fapp);
|
|
|
|
this._log = Log4Moz.repository.getLogger("Snowl.Service");
|
|
this._log.info("initialized logging");
|
|
},
|
|
|
|
_registerFeedHandler: function() {
|
|
if (this._converterSvc.getWebContentHandlerByURI(TYPE_MAYBE_FEED, SNOWL_HANDLER_URI))
|
|
return;
|
|
|
|
this._converterSvc.registerContentHandler(TYPE_MAYBE_FEED,
|
|
SNOWL_HANDLER_URI,
|
|
SNOWL_HANDLER_TITLE,
|
|
null);
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Event & Notification Handlers
|
|
|
|
onSourcesChanged: function() {
|
|
// Invalidate the cache of sources indexed by ID.
|
|
this._sourcesByID = null;
|
|
this._targetsByID = null;
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Accounts, Sources, Targets
|
|
|
|
_accountTypeConstructors: {},
|
|
addAccountType: function(constructor, typeAttributes) {
|
|
if (constructor in this._accountTypeConstructors)
|
|
this._log.warn("constructor for " + constructor.name +
|
|
"already exists");
|
|
this._accountTypeConstructors[constructor.name] = constructor;
|
|
|
|
if (!this.hasSource(constructor.name)) {
|
|
// Create a record for an account type, eq SnowlFeed or SnowlTwitter, that
|
|
// will contain global attributes for that source type.
|
|
this.insertSourceType(constructor.name,
|
|
"SnowlAccountType",
|
|
constructor.name,
|
|
SnowlDateUtils.jsToJulianDate(new Date),
|
|
typeAttributes);
|
|
}
|
|
|
|
// XXX: probably better to do this on a db version change within database.js,
|
|
// but the source type objects don't seem to be ready.
|
|
this.initAccountAttributes(constructor, typeAttributes);
|
|
},
|
|
|
|
get _initAccountAttributesStatement() {
|
|
let statement = SnowlDatastore.createStatement(
|
|
"SELECT id, type, name, machineURI, humanURI, username, " +
|
|
" lastRefreshed, importance, placeID, attributes " +
|
|
"FROM sources " +
|
|
"WHERE type = :type OR machineURI = :type");
|
|
this.__defineGetter__("_initAccountAttributesStatement", function() { return statement });
|
|
return this._initAccountAttributesStatement;
|
|
},
|
|
|
|
// If attributes are added or removed in the global source or individual
|
|
// source types, the db needs to be updated. For existing attributes, the
|
|
// values persisted in the db must be retained.
|
|
initAccountAttributes: function(constructor, typeAttributes) {
|
|
let account = {}, meldAttributes = {}, count = 1;
|
|
|
|
try {
|
|
this._initAccountAttributesStatement.params.type = constructor.name;
|
|
while (this._initAccountAttributesStatement.step()) {
|
|
account = this._constructAccount(this._initAccountAttributesStatement.row);
|
|
meldAttributes = typeAttributes;
|
|
Mixins.meld(account.attributes, false, true, SnowlService._log).into(meldAttributes);
|
|
// Meld any existing db attributes into new typeAttributes; those removed
|
|
// are thus tossed. Set new account attributes back and persist.
|
|
account.attributes = meldAttributes;
|
|
account.persistAttributes();
|
|
}
|
|
}
|
|
finally {
|
|
this._initAccountAttributesStatement.reset();
|
|
}
|
|
|
|
},
|
|
|
|
_constructAccount: function(row) {
|
|
let type = row.type == "SnowlAccountType" ? row.machineURI : row.type;
|
|
let constructor = this._accountTypeConstructors[type];
|
|
if (!constructor)
|
|
throw "no constructor for type " + row.type;
|
|
|
|
return new constructor(row.id,
|
|
row.name,
|
|
URI.get(row.machineURI),
|
|
URI.get(row.humanURI),
|
|
row.username,
|
|
SnowlDateUtils.julianToJSDate(row.lastRefreshed),
|
|
row.importance,
|
|
row.placeID,
|
|
JSON.parse(row.attributes));
|
|
},
|
|
|
|
get _accountsStatement() {
|
|
delete this._accountsStatement;
|
|
return this._accountsStatement = SnowlDatastore.createStatement(
|
|
"SELECT id, type, name, machineURI, humanURI, username, " +
|
|
" lastRefreshed, importance, placeID, attributes " +
|
|
"FROM sources"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Get all accounts. For types SnowlAccountType, store separately.
|
|
*/
|
|
_accountTypesByType: {},
|
|
get accounts() {
|
|
let accounts = [];
|
|
try {
|
|
while (this._accountsStatement.step()) {
|
|
try {
|
|
if (this._accountsStatement.row.type == "SnowlAccountType")
|
|
this._accountTypesByType[this._accountsStatement.row.machineURI] =
|
|
this._constructAccount(this._accountsStatement.row);
|
|
else
|
|
accounts.push(this._constructAccount(this._accountsStatement.row));
|
|
}
|
|
catch(ex) {
|
|
this._log.error(ex);
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
this._accountsStatement.reset();
|
|
}
|
|
|
|
return accounts;
|
|
},
|
|
|
|
get sources() {
|
|
return this.accounts.filter(function(acct) acct.implements(SnowlSource));
|
|
},
|
|
|
|
_sourcesByID: null,
|
|
get sourcesByID() {
|
|
if (!this._sourcesByID) {
|
|
this._sourcesByID = {};
|
|
for each (let source in this.sources)
|
|
this._sourcesByID[source.id] = source;
|
|
}
|
|
|
|
return this._sourcesByID;
|
|
},
|
|
|
|
get targets() {
|
|
return this.accounts.filter(function(acct) acct.implements(SnowlTarget));
|
|
},
|
|
|
|
_targetsByID: null,
|
|
get targetsByID() {
|
|
if (!this._targetsByID) {
|
|
this._targetsByID = {};
|
|
for each (let target in this.targets)
|
|
this._targetsByID[target.id] = target;
|
|
}
|
|
|
|
return this._targetsByID;
|
|
},
|
|
|
|
refreshStaleSources: function() {
|
|
// This used to check SnowlPlaces._placesConverted, but that property
|
|
// doesn't seem to get set to true after SnowlPlaces::delayedInit gets run
|
|
// on startup, and we should always refresh stale sources when requested
|
|
// to do so, even if Places hasn't been converted yet. In fact, we should
|
|
// refresh them even if Places isn't initialized, since only one view
|
|
// in Snowl depends on Places, and the rest of Snowl shouldn't be made
|
|
// to depend on Snowl's Places integration being ready.
|
|
// FIXME: replace this check with one in SnowlPlaces that handles any
|
|
// refreshes that have taken place before the Places integration was
|
|
// initialized/converted.
|
|
if (!SnowlPlaces._placesInitialized) {
|
|
this._log.info("not refreshing stale sources, as Places integration not inited");
|
|
return;
|
|
}
|
|
|
|
this._log.debug("refreshing stale sources");
|
|
|
|
let now = new Date();
|
|
let staleSources = [];
|
|
for each (let source in this.sourcesByID) {
|
|
if (now - source.lastRefreshed > source.refreshInterval &&
|
|
!this.sourcesByID[source.id].busy &&
|
|
source.attributes.refresh["status"] != "paused" &&
|
|
source.attributes.refresh["status"] != "disabled")
|
|
// Do not autorefresh (as opposed to user initiated refresh) if a source
|
|
// is permanently disabled (404 error eg) or paused or busy.
|
|
staleSources.push(source);
|
|
}
|
|
this.refreshAllSources(staleSources);
|
|
},
|
|
|
|
get refreshingCount() {
|
|
return this._refreshingCount ? this._refreshingCount : this._refreshingCount = 0;
|
|
},
|
|
|
|
set refreshingCount(val) {
|
|
return this._refreshingCount = val;
|
|
},
|
|
|
|
refreshAllSources: function(sources) {
|
|
let cachedsource, refreshSources = [];
|
|
let allSources = sources ? sources : this.sourcesByID;
|
|
|
|
if (this.refreshingCount > 0)
|
|
// Do not refresh if any source in the current cycle is not finished.
|
|
return;
|
|
|
|
// Set busy property, reset states.
|
|
for each (let source in allSources) {
|
|
cachedsource = this.sourcesByID[source.id];
|
|
if (cachedsource) {
|
|
cachedsource.busy = true;
|
|
cachedsource.error = false;
|
|
if (cachedsource.attributes.refresh["status"] != "paused")
|
|
cachedsource.attributes.refresh["status"] = "active";
|
|
cachedsource.persistAttributes();
|
|
}
|
|
|
|
refreshSources.push(source);
|
|
this.refreshingCount = ++this.refreshingCount;
|
|
}
|
|
|
|
this._log.debug("refreshAllSources: count - "+this.refreshingCount);
|
|
|
|
if (refreshSources.length > 0)
|
|
// Invalidate collections tree to show new state. Also disable list view
|
|
// refresh button so no db concurrency trouble.
|
|
Observers.notify("snowl:messages:completed", "refresh");
|
|
|
|
// We specify the same refresh time when refreshing sources so that all
|
|
// new messages have the same received time, which makes messages sorted by
|
|
// received, then published times look better (more mixed together by
|
|
// publication time, not clumped up by source based on the received time)
|
|
// when retrieved in the same refresh (f.e. when the user starts their
|
|
// browser in the morning after leaving it off overnight).
|
|
let refreshTime = new Date();
|
|
for each (let source in refreshSources)
|
|
this.refreshSourceTimer(source, refreshTime);
|
|
},
|
|
|
|
refreshSourceTimer: function(aSource, aRefreshTime) {
|
|
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
let callback = { notify: function(aTimer) {
|
|
SnowlService._log.info("Refreshing source: " +
|
|
aSource.name + " - " + aSource.machineURI.spec);
|
|
try {
|
|
aSource.refresh(aRefreshTime);
|
|
}
|
|
catch(ex) {
|
|
aSource.attributes.refresh["text"] = ex;
|
|
aSource.onRefreshError();
|
|
}
|
|
try {
|
|
aSource.persist(true);
|
|
}
|
|
catch(ex) {
|
|
aSource.attributes.refresh["text"] = ex;
|
|
aSource.onDbError();
|
|
}
|
|
} };
|
|
|
|
timer.initWithCallback(callback, 10, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
},
|
|
|
|
|
|
/**
|
|
* Determine whether or not, and by what policy, to mark messages deleted
|
|
* according to individual source and global source type settings.
|
|
*
|
|
*/
|
|
retentionCheck: function() {
|
|
let query, daysOld, daysOldDate, daysOldDateJulian, messagesNumber, keepFlagged;
|
|
let skipTypes = {}, byNumberTypes = {}, typeAttributes;
|
|
let now = new Date();
|
|
|
|
let selectStr = "SELECT id WHERE";
|
|
let acctTypeStr = " sourceID IN (SELECT id from sources WHERE type = ";
|
|
let sourceStr = " sourceID = ";
|
|
let byDaysStr1 = " AND ( CASE WHEN timestamp ISNULL THEN received < ";
|
|
let byDaysStr2 = " ELSE timestamp < ";
|
|
let byDaysStr3 = " END )";
|
|
let byNumberStr1 = " AND id NOT IN ( SELECT id FROM messages WHERE sourceID = ";
|
|
let byNumberStr2 = " ORDER BY id DESC LIMIT ";
|
|
let flaggedStr = " AND id NOT IN ( SELECT id FROM messages WHERE attributes " +
|
|
" REGEXP 'FLAGGED_TRUE' )";
|
|
let deletedStr = " AND ( current = " + MESSAGE_NON_CURRENT + " OR" +
|
|
" current = " + MESSAGE_CURRENT + " )";
|
|
|
|
// Preprocessing for higher probability cases; skip unnecessary per source
|
|
// checks by using broader sql statements.
|
|
for each (let accountType in this._accountTypesByType) {
|
|
//this._log.info("retentionCheck: accountType:attrs - "+accountType.name+" : " +accountType.attributes.toSource());
|
|
query = null;
|
|
if (accountType.attributes.retention.useDefault) {
|
|
if (accountType.attributes.retention.deleteBy == MESSAGE_NODELETE) {
|
|
//this._log.info("retentionCheck: accountType0:query - "+accountType.name+" : " +query);
|
|
skipTypes[accountType.name] = accountType.name;
|
|
// continue;
|
|
}
|
|
if (accountType.attributes.retention.deleteBy == MESSAGE_BYMESSAGESNUMBER) {
|
|
// Delete > number of messages. Must be handled on per source basis..
|
|
byNumberTypes[accountType.name] = accountType.name;
|
|
|
|
//this._log.info("retentionCheck: accountType1:query - "+accountType.name+" : " +query);
|
|
// continue;
|
|
}
|
|
if (accountType.attributes.retention.deleteBy == MESSAGE_BYDAYSOLD) {
|
|
// Delete > days old messages. Convert to julian date stored in db.
|
|
daysOld = accountType.attributes.retention.deleteDays;
|
|
daysOldDate = now - (daysOld * 1000 * 60 * 60 * 24);
|
|
daysOldDateJulian = SnowlDateUtils.jsToJulianDate(new Date(daysOldDate));
|
|
keepFlagged = accountType.attributes.retention.keepFlagged;
|
|
query = selectStr;
|
|
query += acctTypeStr + "'" + accountType.name + "')";
|
|
query += byDaysStr1 + daysOldDateJulian;
|
|
query += byDaysStr2 + daysOldDateJulian;
|
|
query += byDaysStr3;
|
|
query += keepFlagged ? flaggedStr : "";
|
|
query += deletedStr;
|
|
//this._log.info("retentionCheck: accountType2:query - "+accountType.name+" : " +query);
|
|
skipTypes[accountType.name] = accountType.name;
|
|
this.retentionDeleteTimer(accountType.name, query);
|
|
// continue;
|
|
}
|
|
}
|
|
//this._log.info("retentionCheck: accountType:query - "+accountType.name+" : " +query);
|
|
}
|
|
|
|
if (skipTypes.__count__ == this._accountTypesByType.__count__)
|
|
// All account types have useDefault=true, and default is either 'no delete'
|
|
// or 'delete by days old' (handled above).
|
|
return;
|
|
|
|
//this._log.info("retentionCheck: gotAction");
|
|
|
|
/**/
|
|
for each (let source in this.sourcesByID) {
|
|
query = null;
|
|
daysOld = null;
|
|
messagesNumber = null;
|
|
keepFlagged = null;
|
|
|
|
typeAttributes = this._accountTypesByType[source.constructor.name].attributes;
|
|
//this._log.info("retentionCheck: source - "+source.name);
|
|
//this._log.info("retentionCheck: source.constructor.name - " +source.constructor.name);
|
|
if (source.constructor.name in skipTypes ||
|
|
(!source.constructor.name in byNumberTypes &&
|
|
((source.attributes.retention.useDefault &&
|
|
typeAttributes.retention.deleteBy == MESSAGE_NODELETE) ||
|
|
(!source.attributes.retention.useDefault &&
|
|
source.attributes.retention.deleteBy == MESSAGE_NODELETE)))) {
|
|
// The type for this source has already been handled, or useDefault for
|
|
// this source is true and source type default is 'no delete' (source
|
|
// type useDefault override is not set to true), or useDefault is false
|
|
// and setting is 'no delete'.
|
|
//this._log.info("retentionCheck: source No Delete - "+source.name);
|
|
continue;
|
|
}
|
|
//this._log.info("retentionCheck: source.constructor.name - " +source.constructor.name);
|
|
if (source.constructor.name in byNumberTypes) {
|
|
//this._log.info("retentionCheck: source - "+source.name);
|
|
// Default override set for source type, delete by number of messages.
|
|
messagesNumber = typeAttributes.retention.deleteNumber;
|
|
keepFlagged = typeAttributes.retention.keepFlagged;
|
|
}
|
|
else {
|
|
if (source.attributes.retention.useDefault) {
|
|
if (typeAttributes.retention.deleteBy == MESSAGE_BYDAYSOLD)
|
|
daysOld = typeAttributes.retention.deleteDays;
|
|
if (typeAttributes.retention.deleteBy == MESSAGE_BYMESSAGESNUMBER)
|
|
messagesNumber = typeAttributes.retention.deleteNumber;
|
|
keepFlagged = typeAttributes.retention.keepFlagged;
|
|
}
|
|
else {
|
|
if (source.attributes.retention.deleteBy == MESSAGE_BYDAYSOLD)
|
|
daysOld = source.attributes.retention.deleteDays;
|
|
if (source.attributes.retention.deleteBy == MESSAGE_BYMESSAGESNUMBER)
|
|
messagesNumber = source.attributes.retention.deleteNumber;
|
|
keepFlagged = source.attributes.retention.keepFlagged;
|
|
}
|
|
}
|
|
|
|
if (daysOld) {
|
|
// Convert to julian date stored in db.
|
|
daysOldDate = now - (daysOld * 1000 * 60 * 60 * 24);
|
|
daysOldDateJulian = SnowlDateUtils.jsToJulianDate(new Date(daysOldDate));
|
|
query = selectStr;
|
|
query += sourceStr + source.id;
|
|
query += byDaysStr1 + daysOldDateJulian;
|
|
query += byDaysStr2 + daysOldDateJulian;
|
|
query += byDaysStr3;
|
|
query += keepFlagged ? flaggedStr : "";
|
|
query += deletedStr;
|
|
}
|
|
else if (messagesNumber) {
|
|
query = selectStr;
|
|
query += sourceStr + source.id;
|
|
query += byNumberStr1 + source.id;
|
|
query += byNumberStr2 + messagesNumber + " )";
|
|
query += keepFlagged ? flaggedStr : "";
|
|
query += deletedStr;
|
|
}
|
|
|
|
//this._log.info("retentionCheck: source:query - "+source.name+" : " +query);
|
|
if (query)
|
|
this.retentionDeleteTimer(source.name, query);
|
|
}
|
|
|
|
// Refresh the collections tree.
|
|
this._collectionStatsByCollectionID = null;
|
|
Observers.notify("snowl:messages:completed", "refresh");
|
|
},
|
|
|
|
retentionDeleteTimer: function(sourceName, query) {
|
|
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
let callback = { notify: function(aTimer) {
|
|
SnowlService._log.info("Message retention cleanup: " + sourceName + " : " +query);
|
|
try {
|
|
SnowlMessage.markDeletedState(query, true);
|
|
}
|
|
catch(ex) {
|
|
// FIXME: Errors here are likely due to db lock concurrency. Either
|
|
// wait for next cycle or reset interval and try again for X times etc.
|
|
// throw (ex);
|
|
}
|
|
} };
|
|
|
|
timer.initWithCallback(callback, 100, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
},
|
|
|
|
|
|
/**
|
|
* Determine whether or not the datastore contains the message with the given ID.
|
|
*
|
|
* @param aExternalID {string} the external ID of the message
|
|
*
|
|
* @returns {boolean} whether or not the datastore contains the message
|
|
*/
|
|
hasMessage: function(aExternalID) {
|
|
return SnowlDatastore.selectHasMessage(aExternalID);
|
|
},
|
|
|
|
/**
|
|
* Determine whether or not an author has at least one message in the database.
|
|
*
|
|
* @param aAuthorID {string} the author ID of the message
|
|
*
|
|
* @returns {boolean} whether or not the author has a message
|
|
*/
|
|
hasIdentityMessage: function(aAuthorID) {
|
|
return SnowlDatastore.selectHasIdentityMessage(aAuthorID);
|
|
},
|
|
|
|
/**
|
|
* Determine whether or not an author has at least one identity in the database.
|
|
*
|
|
* @param aAuthorID {string} the identity ID of the message
|
|
*
|
|
* @returns {boolean} whether or not the author has an identity
|
|
*/
|
|
hasAuthorIdentity: function(aAuthorID) {
|
|
return SnowlDatastore.selectHasAuthorIdentity(aAuthorID);
|
|
},
|
|
|
|
/**
|
|
* Determine whether or not the datastore contains a source with the given URI.
|
|
*
|
|
* @param aMachineURI {string} the URI to check
|
|
*
|
|
* @returns {string} the name of the source if found otherwise null
|
|
*/
|
|
hasSource: function(aMachineURI) {
|
|
return SnowlDatastore.selectHasSource(aMachineURI);
|
|
},
|
|
|
|
/**
|
|
* Determine whether or not the datastore contains a source with the given URI
|
|
* also username for multiple subscriptions per URI. Use hasSource() for URI
|
|
* only check.
|
|
*
|
|
* @param aMachineURI {string} the URI to check
|
|
* @param aUsername {string} the username to check
|
|
*
|
|
* @returns {object} the [name, username] of the source and username if found
|
|
* otherwise nulls
|
|
*/
|
|
hasSourceUsername: function(aMachineURI, aUsername) {
|
|
return SnowlDatastore.selectHasSourceUsername(aMachineURI, aUsername);
|
|
},
|
|
|
|
/**
|
|
* Store into sources a SnowlAccountType record, for each type of source, to
|
|
* contain default attributes.
|
|
*
|
|
* @param aName {string} the name
|
|
* @param aType {string} the system source type 'SnowlAccountType'
|
|
* @param aMachineURI {string} the type of source
|
|
* @param aLastRefreshed {date} the date
|
|
* @param aAttributes {string} the JSON attribute string
|
|
*
|
|
* @returns {integer} the id of the new record.
|
|
*/
|
|
insertSourceType: function(aName, aType, aMachineURI, aLastRefreshed, aAttributes) {
|
|
return SnowlDatastore.insertSourceType(aName, aType, aMachineURI, aLastRefreshed, aAttributes);
|
|
},
|
|
|
|
/**
|
|
* Return read, unread, new stats on author, source collections.
|
|
*
|
|
* @returns {object} the t (total), u (unread), n (new) numbers for each
|
|
* source and author collection.
|
|
*/
|
|
_collectionStatsByCollectionID: null,
|
|
getCollectionStatsByCollectionID: function() {
|
|
if (!this._collectionStatsByCollectionID)
|
|
this._collectionStatsByCollectionID = SnowlDatastore.collectionStatsByCollectionID();
|
|
|
|
return this._collectionStatsByCollectionID;
|
|
}
|
|
|
|
};
|
|
|
|
SnowlService._init();
|