0.8/components/notify.js

Back to the directory /*
    --------------------------------------------------------
    Notify
    --------------------------------------------------------
    
    Copyright (C) 2006, Gemme.pl
    http://notify.torino.pl/en/
    All rights reserved.
    
    See attached file: LICENCE.TXT
    
    --------------------------------------------------------
*/

// #################################################################
//                             Preferences
// #################################################################

function NotifySitesPreferences() {
    
    this.parent = null; // CNotify object
    this.prefs = null;
    this.PrefService = null;
    
    this.addObservedSite = function(aURL)
    {
        var s = new NotifyWatchSite();
        s.URL = aURL;
        s.LastChange = new Date();
        s.ServerIsKind = false; // TODO: check, use HTTP 1.1
        
        var sites = this.prefs.getCharPref("observedSites");
        if (sites != "") {
            sites += "|" + s.toString();
        } else {
            sites = s.toString();
        }
        
        this.prefs.setCharPref("observedSites", sites);
        this.PrefService.savePrefFile(null); // save to disk
        
        // NotifyPreferences.refreshSites() should go now
    }
    
    this.addCalendarSite = function(aURL, aYear, aMonth, aDay)
    // aMonth is 1..12
    {
        var s = new NotifyCalendarSite();
        s.URL = aURL;
        var d = new Date(aYear, aMonth - 1, aDay, 0, 0, 0, 0);
        s.When = d.getTime();
        s.Opened = false;
        
        var sites = this.prefs.getCharPref("calendarSites");
        if (sites != "") {
            sites += "|" + s.toString();
        } else {
            sites = s.toString();
        }
        
        this.prefs.setCharPref("calendarSites", sites);
        this.PrefService.savePrefFile(null); // save to disk
        
        // NotifyPreferences.refreshSites() should go now
    }
    
    this.deleteObservedSite = function(x, saveAfterDelete)
    {
        delete this.parent.ObservedSites[x];
        
        if (saveAfterDelete)
        {
            this.saveObservedSites();
        }
    }
    
    this.deleteCalendarSite = function(x, saveAfterDelete)
    {
        delete this.parent.CalendarSites[x];
        
        if (saveAfterDelete)
        {
            this.saveCalendarSites();
        }
    }
    
    this.saveObservedSites = function()
    {
        // this.parent.ObservedSites.join("|") sucks!
        var sites = "";
        for (var el in this.parent.ObservedSites) {
            sites += this.parent.ObservedSites[el].toString() + "|";
        }
        if (sites.length) {
            sites = sites.substr(0, sites.length-1); // last "|"
        }
        
        this.prefs.setCharPref("observedSites", sites);
        this.PrefService.savePrefFile(null); // save to disk
    }
    
    this.saveCalendarSites = function()
    {
        // this.parent.CalendarSites.join("|") sucks!
        var sites = "";
        for (var el in this.parent.CalendarSites) {
            sites += this.parent.CalendarSites[el].toString() + "|";
        }
        if (sites.length) {
            sites = sites.substr(0, sites.length-1); // last "|"
        }
        
        this.prefs.setCharPref("calendarSites", sites);
        this.PrefService.savePrefFile(null); // save to disk
    }
    
} // NotifySitesPreferences

function NotifyPreferences() {

    this.prefs = null;
    this.PrefService = null;
    this.parent = null; // CNotify object
    this.sites = null;
    
    // some preference in our branch has changed
    this.observe = function(subject, topic, data)
    {
        if (topic != "nsPref:changed") {
            return;
        }
        
        switch(data)
        {
            case "calendarSites":
            case "observedSites": // TODO: test
                this.refreshSites();
                break;
            case "sound":
            case "tray":
            case "open":
                this.refreshAlarms();
                break;
            case "period":
                this.refreshTimer();
                break;
        }
    }
    
    this.refreshTimer = function()
    {
        this.parent.timerInterval = Math.max(1, this.prefs.getIntPref("period")) /* min */ * 60 * 1000;
        this.parent.setTimer();
    }
    
    this.refreshAlarms = function()
    {
        this.parent.Alarm.open = this.prefs.getBoolPref("open");
        this.parent.Alarm.tray = this.prefs.getBoolPref("tray");
        this.parent.Alarm.sound = this.prefs.getBoolPref("sound");
    }
    
    this.refreshSites = function()
    {
        delete this.parent.ObservedSites;
        delete this.parent.CalendarSites;
        
        this.parent.ObservedSites = new Array();
        this.parent.CalendarSites = new Array();
        
        
        var s = this.prefs.getCharPref("observedSites");
        var a = s.split("|");
        for (var el in a)
        {
            if (a[el] != "")
            {
                site = new NotifyWatchSite();
                site.loadFromString(a[el]);
                this.parent.ObservedSites.push(site);
            }
        }
        
        s = this.prefs.getCharPref("calendarSites");
        a = s.split("|");
        for (var el in a)
        {
            if (a[el] != "")
            {
                site = new NotifyCalendarSite();
                site.loadFromString(a[el]);
                this.parent.CalendarSites.push(site);
            }
        }
    }
    
    this.init = function()
    {
        this.refreshAlarms();
        this.refreshSites();
        this.refreshTimer();
    }
    
    
    // main constructor code
    this.PrefService =
        Components.classes["@mozilla.org/preferences-service;1"]
        .getService(Components.interfaces.nsIPrefService);
    this.prefs = 
        this.PrefService.getBranch("extensions.notify.");
    this.prefs.QueryInterface(Components.interfaces.nsIPrefBranch2);
    this.prefs.addObserver("", this, false);
    
    this.sites = new NotifySitesPreferences(this);
    this.sites.PrefService = this.PrefService;
    this.sites.prefs = this.prefs;
    
}; // NotifyPreferences


// #################################################################
//                                 Sites
// #################################################################

function NotifySite() {
    
    this.URL = "";
    
    this.toString = function() // abstract
    {
        // result must not contain "|"
    }
    
    this.loadFromString = function(s) // abstract
    {
    }
    
}

function NotifyWatchSite() {
    
    this.URL = "";
    this.LastChange = null; // Date object
    this.ServerIsKind = false; // provides Last-Modified and ETag headers
    this.Hash = "";
    this.ETag = "";
    
    this.toString = function()
    {
        // <1,"url","date","Hash","ETag">
        //  1 - if ServerIsKind, otherwise 0
        var s =
            "<" +
            ((this.ServerIsKind == true) ? "1" : "0") +
            ",\"" +
            this.URL +
            "\",\"" +
            this.LastChange +
            "\",\"" +
            this.Hash +
            "\",\"" +
            this.ETag +
            "\">";
        return s;
    }
    this.loadFromString = function(s)
    {
        var URLEnd = s.indexOf("\"", 4);
        var LastChangeEnd = s.indexOf("\"", URLEnd + 3);
        var HashEnd = s.indexOf("\"", LastChangeEnd + 3);
        var ETagEnd = s.indexOf("\"", HashEnd + 3);
        
        this.ServerIsKind = (s[1] == "1");
        this.URL =
            s.substring(/* after <" */ 4, URLEnd);
        this.LastChange =
            new Date(s.substring(URLEnd + 3, LastChangeEnd));
        this.Hash =
            s.substring(LastChangeEnd + 3, HashEnd);
        this.ETag =
            s.substring(HashEnd + 3, ETagEnd);
    }
    
}
NotifyWatchSite.prototype = new NotifySite();

function NotifyCalendarSite() {
    
    this.URL = "";
    this.When = 0; // date.getTime()
    this.Opened = false;
    
    this.toString = function()
    {
        // <1,"url","date - getTime()">
        //  1 - if already opened, otherwise 0
        
        var s =
            "<" +
            ((this.Opened == true) ? "1" : "0") +
            ",\"" +
            this.URL +
            "\",\"" +
            this.When +
            "\">";
        return s;
    }
    this.loadFromString = function(s)
    {
        var URLEnd = s.indexOf("\"", 4);
        this.Opened = (s[1] == "1");
        this.URL =
            s.substring(/* after <1," */ 4, URLEnd);
        this.When =
            parseInt(s.substring(URLEnd + 3, s.indexOf("\"", URLEnd + 3)));
    }
    
}
NotifyCalendarSite.prototype = new NotifySite();

// #################################################################
//                                 Alarm
// #################################################################

function NotifyAlarm() {
    
    this.parent = null;
    
    this.open = true;
    this.tray = true;
    this.sound = true;
    
    this.alarm = function(Site, AlwaysOpen, IsFromCalendar)
    {
        if (this.open || AlwaysOpen)
        {
            if (Site.toLowerCase().indexOf("http://") != 0)
            {
                Site = "http://" + Site;
            }
            
            var url =
                Components.classes["@mozilla.org/supports-string;1"]
                .createInstance(Components.interfaces.nsISupportsString);
            url.data = Site;
            
            var ww =
                Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
                .getService(Components.interfaces.nsIWindowWatcher);
            ww.openWindow(null, "chrome://browser/content/browser.xul", "_blank",
                "chrome,all,dialog=no", url);
        }
        if (this.tray)
        {
            // check if the alert service is present
            if (!("@mozilla.org/alerts-service;1" in Components.classes))
                return;
            
            var s;
            if (IsFromCalendar) {
                s = this.parent.StrBundle.GetStringFromName("TrayMessageTitleCalendar"); // TODO: user's message?
            }
            else
            {
                s = this.parent.StrBundle.GetStringFromName("TrayMessageTitleNotify");
            }
            
            
            var AlertService =
                Components.classes["@mozilla.org/alerts-service;1"]
                .getService(Components.interfaces.nsIAlertsService);
            
            AlertService.showAlertNotification(
                "chrome://notify/content/emblem-important.png", s, Site, true, "", null);
            
            // TODO:
            /* adopt this ....
            You can get notified when the notification window disappears or user clicks on the message by passing an object implementing nsIObserver as the last parameter:
            
            var listener = {
              observe: function(subject, topic, data) {
                alert("subject=" + subject + ", topic=" + topic + ", data=" + data);
              }
            }
            var alertsService = Components.classes["@mozilla.org/alerts-service;1"]
                                          .getService(Components.interfaces.nsIAlertsService);
            alertsService.showAlertNotification("",  "Alerts service test", "Click me", 
                                                true, "cookie", listener);
            */
        }
        if (this.sound)
        {
            const SoundFile = "Notify.wav";            // TODO: pref?
            const NotifyId  = "notify@torino.pl";
            
            var SoundPath =
                Components.classes["@mozilla.org/extensions/manager;1"]
                .getService(Components.interfaces.nsIExtensionManager)
                .getInstallLocation(NotifyId)
                .getItemLocation(NotifyId);
            SoundPath.append(SoundFile);
            
            var sound = Components.classes["@mozilla.org/sound;1"]
                .createInstance(Components.interfaces.nsISound);
            
            var filePath =
                new Components.Constructor(
                    "@mozilla.org/file/local;1",
                    "nsILocalFile", "initWithPath");
            var file = new filePath(SoundPath.path);
            var ioService =
                Components.classes['@mozilla.org/network/io-service;1']
                .getService(Components.interfaces.nsIIOService);
            var url = ioService.newFileURI(file, null, null);
            
            sound.play(url);
        }
    }
    
} // NotifyAlarm


// #################################################################
//                                 Main
// #################################################################

// interfaces we support
const nsINotify        = Components.interfaces.nsINotify;
const nsISupports    = Components.interfaces.nsISupports;
const nsITimer        = Components.interfaces.nsITimer;
const nsIObserver    = Components.interfaces.nsIObserver;

// class constants
const CLASS_ID = Components.ID("{12E355B4-EE49-11DA-9B41-B622A1EF5492}");
const CLASS_NAME = "Notify Extension Component";
const CONTRACT_ID = "@torino.pl/notify;1";
const OBSERVER_NAME = "Notify Extension Observer";

const OBSERVER_CONTRACT_ID = "@mozilla.org/observer-service;1";
const TIMER_CONTRACT_ID = "@mozilla.org/timer;1";
const HASH_CONTRACT_ID = "@mozilla.org/security/hash;1";
const nsICryptoHash = Components.interfaces.nsICryptoHash;

const BUNDLE = "chrome://notify/locale/notify.properties";


// class constructor
function CNotify()
{
    this.initService();
};

// class definition
CNotify.prototype = {
    
    StrBundle: null,
    Timer: null,
    
    ObservedSites: null,
    CalendarSites: null,
    Alarm: null,
    Prefs: null,
    
    timerInterval: 15 /* min */ * 60 * 1000,
    // changed in this.Prefs.init(); anyway
    
    initService : function()
    {
        var obs = Components.classes["@mozilla.org/observer-service;1"]
            .getService(Components.interfaces.nsIObserverService);
        obs.addObserver(this, "profile-after-change", false);
    },
    
    init : function()
    {
        this.wrappedJSObject = this;
        
        var StrBundleService =
            Components.classes["@mozilla.org/intl/stringbundle;1"]
            .createInstance(Components.interfaces.nsIStringBundleService);
        this.StrBundle = StrBundleService.createBundle(BUNDLE);
        
        this.ObservedSites = new Array();
        this.CalendarSites = new Array();
        
        this.Alarm = new NotifyAlarm();
        this.Alarm.parent = this;
        
        this.Prefs = new NotifyPreferences();
        this.Prefs.parent = this;
        this.Prefs.sites.parent = this;
        this.Prefs.init();
        
        //this.Prompt = Components.classes["@mozilla.org/network/default-prompt;1"].createInstance(Components.interfaces.nsIPrompt);
        //this.Prompt.alert('test', 'start');
        
        this.setTimer();
        this.checkSites();
    },
    
    checkSites : function()
    {
        for (var el in this.ObservedSites)
        {
            var Request =
                Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
                .createInstance(Components.interfaces.nsIXMLHttpRequest);
            /*
                http://forums.mozillazine.org/viewtopic.php?t=427229
                http://www.mail-archive.com/dev-tech-xpcom@lists.mozilla.org/msg00264.html
                So we need this line:
            */
            Request instanceof Components.interfaces.nsIJSXMLHttpRequest;
            
            Request.open("GET", this.ObservedSites[el].URL, true);
            // TODO: HEAD, "the correct way"]
            
            var Notify = this;
            var func =
                eval(
                    "function(event) \
                    { \
                        Notify.siteChecked(event, " + el + "); \
                    };"
                    );
            Request.onload = func;
            
            // don't get a copy from cache
            Request.channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;
            // don't slow down normal browser activity
            if (Request.channel instanceof Components.interfaces.nsISupportsPriority)
                Request.channel.priority = Components.interfaces.nsISupportsPriority.PRIORITY_LOWEST;
            // above 2 tricks thanks to this thread: http://forums.mozillazine.org/viewtopic.php?t=397699
            
            Request.send(null);
        }
        
        var nowDate = new Date();
        var now = nowDate.getTime();
        var changed = false;
        
        for (var el in this.CalendarSites)
        {
            if ( !this.CalendarSites[el].Opened && (now >= this.CalendarSites[el].When) )
            {
                this.Alarm.alarm(this.CalendarSites[el].URL, true, true);
                
                // don't alarm about the site anymore
                this.CalendarSites[el].Opened = true;
                changed = true;
                
                // TODO: option - delete it?
            }
        }
        if (changed) {
            this.Prefs.sites.saveCalendarSites();
        }
    },
    
    siteChecked : function(event, el)
    {
        var Request = event.target;
        
        if (Request.status != 200)
        {
            // TODO: should we warn the user?
            // TODO: 3xx redirections
            return;
        }
        
        this.ObservedSites[el].ServerIsKind = (Request.getResponseHeader("Last-Modified") != ""); // TODO: works?
        
        // compute hash
        var slength = Request.responseText.length;
        var StringStream =
            Components.classes["@mozilla.org/io/string-input-stream;1"]
                .createInstance(Components.interfaces.nsIStringInputStream);
        StringStream.setData(Request.responseText, slength);
        
        var HashService = Components.classes[HASH_CONTRACT_ID].createInstance(nsICryptoHash);
        HashService.init(Components.interfaces.nsICryptoHash.MD5);
        
        HashService.updateFromStream(StringStream, slength);
        Hash = HashService.finish(true);
        
        if (this.ObservedSites[el].Hash != Hash)
        {
            // empty old hash => don't alarm
            if (this.ObservedSites[el].Hash.length) {
                this.Alarm.alarm(this.ObservedSites[el].URL, false, false);
            }
            this.ObservedSites[el].Hash = Hash;
            this.Prefs.sites.saveObservedSites();
        }
    },
    
    setTimer : function()
    {
        if (this.Timer)
        {
            this.Timer.cancel();
        }
        else
        {
            this.Timer = Components.classes[TIMER_CONTRACT_ID].createInstance(nsITimer);
        }
        this.Timer.initWithCallback(this, this.timerInterval, nsITimer.TYPE_REPEATING_SLACK);
    },
    
    // nsITimer
    notify : function(aTimer)
    {
        this.checkSites();
    },
    
    // nsISupports
    QueryInterface : function(aIID)
    {
        if (   !aIID.equals(nsINotify)
            && !aIID.equals(nsISupports)
            && !aIID.equals(nsIObserver)
            && !aIID.equals(nsITimer)
           )
            throw Components.results.NS_ERROR_NO_INTERFACE;
        return this;
    },
    
    // nsIObserver
    observe: function(aSubject, aTopic, aData)
    {
        switch(aTopic)
        {
            case "xpcom-startup":
                var obsSvc = Components.classes[OBSERVER_CONTRACT_ID].getService(nsIObserverService);
                obsSvc.addObserver(this, "profile-after-change", true);
                break;
            
            case "profile-after-change":
                this.init();
                break;
        }
    } 
    
}; // CNotify.prototype



// #################################################################
//                             Component code
// #################################################################
// (modified from:
//    http://forums.mozillazine.org/viewtopic.php?t=308369
//    http://developer.mozilla.org/en/docs/How_to_Build_an_XPCOM_Component_in_Javascript
//    http://kb.mozillazine.org/Implementing_XPCOM_components_in_JavaScript
//    YMail Extension
// )

var Module = {

    myCID: CLASS_ID,
    myProgID: CONTRACT_ID,
    myFriendlyName: CLASS_NAME,
    
    registerSelf: function(compMgr, fileSpec, location, type)
    {
        compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
        
        compMgr.registerFactoryLocation(
            this.myCID, this.myFriendlyName, this.myProgID, fileSpec, location, type
            );
        
        Components.classes["@mozilla.org/categorymanager;1"]
            .getService(Components.interfaces.nsICategoryManager)
            .addCategoryEntry(
                "app-startup", 
                this.myFriendlyName, 
                "service," + this.myProgID, 
                true, true
            );
    },
    
    unregisterSelf: function(compMgr, location, loader)
    {
        Components.classes["@mozilla.org/categorymanager;1"]
            .getService(Components.interfaces.nsICategoryManager)
            .deleteCategoryEntry(
                "app-startup", 
                this.myFriendlyName, 
                true
            );
    },
    
    getClassObject: function(compMgr, cid, iid)
    {
        if(!cid.equals(this.myCID))
            throw Components.results.NS_ERROR_NO_INTERFACE;
        
        if(!iid.equals(Components.interfaces.nsIFactory))
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        
        return this.myFactory;
    },
    
    myFactory: {
        createInstance: function(outer, iid)
        {
            if (outer != null)
                throw Components.results.NS_ERROR_NO_AGGREGATION;
            
            return (new CNotify()).QueryInterface(iid);
        }
    },
    
    canUnload: function(compMgr)
    {
        return true;
    }
    
};

function NSGetModule(compMgr, fileSpec)
{
    return Module;
}
© 2006-2007 Zespół Gemme - Programy na zamówienie