Shoutbox

[IDEA] plusQuery - Printable Version

-Shoutbox (https://shoutbox.menthix.net)
+-- Forum: MsgHelp Archive (/forumdisplay.php?fid=58)
+--- Forum: Messenger Plus! for Live Messenger (/forumdisplay.php?fid=4)
+---- Forum: Scripting (/forumdisplay.php?fid=39)
+----- Thread: [IDEA] plusQuery (/showthread.php?tid=95002)

[IDEA] plusQuery by Amec on 07-13-2010 at 04:43 AM

Hello. I was wondering if anyone had thought of making a jQuery-esque library for Messenger Plus! Live. I believe this would make it much easier and quicker to develop scripts, much like jQuery is for web dev.

For example, something like the following could be used as a simple googling script: (minus some parsing)

code:
$("ChatWnd").receiveMessage(
    function() {
        if(this.message.substr(0, 7) === "!google") {
            var wnd = this;
            $.get(
                "http://www.google.com?q=" + this.message.substr(9, this.message.length - 9),
                function(data) {
                    //parse data here
                    wnd.send(data);
                }
            );
        }
    }
);

I was thinking we could store everything in like an xml data structure, and access it via $(). Like, $("ChatWnd") would select all the ChatWnds, $("ScriptMenu") would select the menu, etc... Or we could do it like $.chatWnd and $.scriptMenu or something... I really don't know atm.

Would anyone be interested in working on this with me? ;)

Edit: Let me clarify on "we could store everything in like an xml data structure".

An example with a LOT of stuff missing:
code:
<Root>
    <ChatWnds>
        <ChatWnd>
            <Handle>{ json rep for HWND object, etc. }</Handle>
            <Contacts>
                <Contact>
                    <Blocked>false</Blocked>
                    <Email>lol@u.com</Email>
                    <Name>Amec</Name>
                    <PersonalMessage>Yo.</PersonalMessage>
                </Contact>
                <Contact>
                    <Blocked>false</Blocked>
                    <Email>lol@me.com</Email>
                    <Name>Yep</Name>
                    <PersonalMessage>Bro.</PersonalMessage>
                </Contact>
            </Contacts>
        </ChatWnd>
    </ChatWnds>
</Root>

One could do $("Contact") to get a Contacts object, or $("Contact > Email:contains('lol')") to get a list of Contacts whose email has 'lol' in it. And this shouldn't just apply to ChatWnds, every data structure (Debug, Messenger, MsgPlus, Emoticons, PlusWnd, Interop, ScriptCommands, ScriptMenu, etc.) should be accessible in this structure, which is updated at run time when data changes...
RE: [IDEA] plusQuery by Spunky on 07-13-2010 at 06:29 AM

I like the idea, but it'd be hard to implement I think. Then you either have to ship the file with every script that uses it giving you multiple copies on the same user's PC, or find a way to download it from an online source and execute it into the current script.


RE: [IDEA] plusQuery by Matti on 07-13-2010 at 07:55 AM

I've had a similar idea once, mainly because I would love to assign and remove event handlers like in jQuery (with bind() and unbind()).

However, if you would want to actually make such framework for Plus! scripts, you're going to need to find a way to create regular OnEvent_* functions in your script and mirror these to your framework. After some testing, I found that it was possible to create these in run-time by manually creating an extra script file and loading that with MsgPlus.LoadScriptFile. The problem is that you can't assign window event handlers without knowing their IDs to create OnWindowIdEvent_* handlers. To get these IDs, you'd need to ask the user of your framework to name all used windows in your script at initialization and whenever the user adds an extra window, he/she must remember to add it into the list.

One solution could be to ask for the paths to the interface XML files and collect all window IDs at initialization. The result of this scanning process could then be cached inside ScriptInfo.xml. The created script file with all mirrored events could be cached in a similar fashion. However these are all nasty workarounds, it is tough to do it right on top of the current scripting environment.

As for selecting objects with CSS-like queries, I don't think scripts would benefit from it that much. In the HTML DOM, it's good to have a selector engine since the current JavaScript functions aren't sufficient for modern scripts and there are many browser inconsistencies to overcome when crawling through the DOM. It's also a very logical way to select elements with CSS-like queries, since we're used to do that in CSS and thus we know exactly what we should expect.

I doubt this query language would make sense in Plus! scripting. Surely, you could have object types as tag names, attribute selectors and descendants, but why would you need all that? I think it's good to be able to plusQuerify an object and dynamically bind events on it, but why do you need to be able to select these objects like that? Most of the time, Plus! already gives you the object you need as event parameter, so just $(ChatWnd) should be enough.

quote:
Originally posted by Spunky
I like the idea, but it'd be hard to implement I think. Then you either have to ship the file with every script that uses it giving you multiple copies on the same user's PC, or find a way to download it from an online source and execute it into the current script.
I think such a library shouldn't get much larger than 10 to 15 kB when properly minified. Besides, if it's a good library which really makes scripting easier, the file size doesn't matter that much anyway.
RE: [IDEA] plusQuery by CookieRevised on 07-13-2010 at 11:47 PM

quote:
Originally posted by Matti
...which really makes scripting easier
... bigger, slower, more prone to errors, very hard to debug, beginning scripters don't even know what they actually are doing (and thus asking more questions as to why something doesn't work), etc....
RE: [IDEA] plusQuery by Matti on 07-14-2010 at 10:28 AM

quote:
Originally posted by CookieRevised
... bigger, slower, more prone to errors, very hard to debug, beginning scripters don't even know what they actually are doing (and thus asking more questions as to why something doesn't work), etc....
That's what you could say about any abstraction layer: jQuery, .NET,... :P

The extra file weight shouldn't be so much of a problem for modern computers and Internet connections, nor should the difference in performance be very noticeable if the library creator knows what he's doing.

I agree that the use of a framework can make debugging tougher, it adds an extra abstraction layer between developer and machine so the chance that you're doing something wrong may increase. However I think the chance of doing something wrong is bigger when the developer has to write a whole function by himself then when he calls a library function which is already thoroughly tested.

You're probably right that beginning developers will tend to jump straight onto a library without learning the underlying language. I didn't learn C++ before C# either so I'm also in that same boat, but I started with JavaScript so I already knew about the basics of programming. I've seen people jump straight into jQuery who don't know anything about the basic structures of JavaScript. This also applies to Plus! scripting on top of JScript: newcomers try to do two things when a message is send, so they define two OnEvent_ChatWndSendMessage functions... Perhaps a library could help here.

I understand your worries about using frameworks. Perhaps a better solution would be that the Plus! scripting environment itself gets updated to comply with our needs (I really want better event handling!), so we don't need libraries to give us what the scripting environment can't do. And if that doesn't happen, I could just make a small private framework for my own scripts without releasing it separately. If there's something wrong with the library, then I can still just update my own scripts and don't need to worry about other developers using my library.

Note to self: write shorter posts!
RE: [IDEA] plusQuery by Amec on 04-06-2011 at 04:34 AM

lol really old post. So uhh... Since no one seemed interested, I decided to do this myself! Although, I did change it a lot from my original suggestion as I became a better ECMAScript "programmer"...

Source: https://github.com/tyscorp/plusquery/

Suggestions/additions/criticisms are welcome. ;)

Edit: I guess I should have included more information...

Wrapper objects
It includes a set of "wrapper objects", which allow the interfaces to be extended. For example, you can overload ChatWnd.SendMessage to do other stuff...

Event Handling
It includes functions for binding/unbinding event listeners, to any plusQuery object. If it is bound to the main plusQuery object, it is always executed when the event is called. If it is bound to a plusQuery( * ) object, it is executed whenever the objects match.
So, if you bind "ChatWndSendMessage" to plusQuery, the function you specify will get called each time you send a message. If you bind it to a plusQuery(ChatWnd) object, the function is only called when you send a message from that ChatWnd object. (binding to instances is buggy atm. >_>)

Timers
It has setTimeout/setInterval/clearTimeout/clearInterval. Based on a mixture of mine and Matti's ideas.

Is also extends Object and Array a lot currently; I will be moving this to the plusQuery object. (except for the delicious Function.prototype.bind ;D)

The latest build is attached.


RE: [IDEA] plusQuery by Matti on 04-06-2011 at 09:43 AM

That is truly amazing! :O

Here's a suggestion: use combined getter/setter methods for the properties in the wrapper classes. Instead of storing this.original.Size in this.Size, make a method Size which can be called as a getter with Size() or as a setter with Size(newSize). Right now every wrapped property is read-only, although some properties should be read/write such as DataBloc.Size or Messenger.MyStatus.

Example:

js code:
$.wrappers.DataBloc.prototype = {
    Size: function (newSize) {
        if( typeof newSize !== "undefined" ) {
            this.original.Size = newSize;
        }
        return this.original.Size;
    }
}
Another thing I noticed is that you're extending Object.prototype. While this may work just fine in your library when using your version of Object.prototype.forEach, this surely will break a lot of scripts which iterate over plain objects using a standard for...in loop without the Object.prototype.hasOwnProperty check. I think it's better to simply drop the prototype extensions and only extend the static Object. This is not so much of an issue with the Array and String prototypes, as those should never be iterated over with for...in anyway.

The thing I miss is (of course) event binding on PlusWnd objects. If this were possible, we could write classes to work with window controls which can bind the necessary handlers to the used window events. Now, I understand that this is very tough to implement and would probably require a lot of hackish constructions. I found a way to inject function in the global object in run-time, but it's very, very ugly. :P Hopefully the event architecture gets fixed/improved in some future version of Plus!...

This deserves its own thread though... :P
RE: RE: [IDEA] plusQuery by Amec on 04-06-2011 at 11:12 AM

quote:
Originally posted by Matti
That is truly amazing! :O
Thanks. :D

quote:
Originally posted by Matti
Here's a suggestion: use combined getter/setter methods for the properties in the wrapper classes. Instead of storing this.original.Size in this.Size, make a method Size which can be called as a getter with Size() or as a setter with Size(newSize). Right now every wrapped property is read-only, although some properties should be read/write such as DataBloc.Size or Messenger.MyStatus.
Hmm, I just tested this... I guess it's something I overlooked. Oops! I'll change them all to the combined getter/setter functions as you suggested soon.

quote:
Originally posted by Matti
Another thing I noticed is that you're extending Object.prototype. While this may work just fine in your library when using your version of Object.prototype.forEach, this surely will break a lot of scripts which iterate over plain objects using a standard for...in loop without the Object.prototype.hasOwnProperty check. I think it's better to simply drop the prototype extensions and only extend the static Object. This is not so much of an issue with the Array and String prototypes, as those should never be iterated over with for...in anyway.
Yeah, I realised that was terrible after I started to REALLY understand ECMAScript... Haven't had time to move them from Object to plusQuery yet. And people who don't check for hasOwnProperty while iterating are silly. But... I guess I have to take that into account. :P

quote:
Originally posted by Matti
The thing I miss is (of course) event binding on PlusWnd objects. If this were possible, we could write classes to work with window controls which can bind the necessary handlers to the used window events. Now, I understand that this is very tough to implement and would probably require a lot of hackish constructions. I found a way to inject function in the global object in run-time, but it's very, very ugly. :P Hopefully the event architecture gets fixed/improved in some future version of Plus!...
Yeah, I thought about it for a bit, and had no idea. The global variable in JScript seems REALLY screwey. :( Doesn't seem to be a way that I can see which adds properties to it.
js code:
function OnMyWndEvent_Cancel(plusWnd) {
    $.trigger(plusWnd, "MyWndEvent_Cancel", plusQuery.$A(arguments));
}

$.addEventListener("MyWndEvent_Cancel", function (plusWnd) {
    //do stuff here
});

$.addEventListener("MyWndEvent_Cancel", function (plusWnd) {
    //do other stuff here
});
It would be so much easier if you could just add it to the global variable. :( How did you manage to do it?
RE: [IDEA] plusQuery by matty on 04-06-2011 at 12:46 PM

Simply amazing!


RE: [IDEA] plusQuery by Amec on 04-06-2011 at 02:07 PM

...oh hey. I just thought of something... It can be done using eval. (feeling kinda sick just by saying that)

js code:
var plusWnd = $(MsgPlus).CreateWnd("interface.xml", "yiew", 0);
//[...]
$.wrappers.MsgPlus.CreateWnd = function (XmlFile, WindowId, Options) {
    var toEval = "var On" + WindowId + "Event_Cancel = function (plusWnd) { $.trigger(plusWnd, '" + WindowId + "Event_Cancel', plusQuery.$A(arguments)); };";
    toEval += "var On" + WindowId + "Event_Destroyed = function (plusWnd, exitCode) { $.trigger(plusWnd, '" + WindowId + "Event_Destroyed', plusQuery.$A(arguments)); };";
    //etc
    (1, eval)(toEval); //indirect call to eval; makes it evaluate in the global scope
    this.original.CreateWnd(XmlFile, WindowId, Options);
}

This would work, no? :)
RE: [IDEA] plusQuery by Matti on 04-06-2011 at 03:03 PM

The solution I came up with also uses eval() as there's no global object in JScript which stores the global variables. (In browser JavaScript, there's the global object "window" which holds the global scope.)
However, you'll have to do one extra thing to make it work...

When adding globals using eval() in run-time, the scripting engine will not register those added functions as event handlers. That is, if you add an OnWndIdEvent_Destroyed() event using eval(), it won't get called when the window is destroyed. The trick to get these registered is to use MsgPlus.LoadScriptFile(). When calling LoadScriptFile, the script file is loaded and the global scope is rescanned for event handlers. This results in the newly added functions to be properly registered. :)

Note that you don't need to trick eval() to be called in the global scope. By simply omitting the "var" keyword, the variable will be created in the global scope. It is generally a bad idea to omit the "var" keyword, but in this case it's sort of acceptable. :P

Proof of concept:

js code:
var plusWnd = $(MsgPlus).CreateWnd("interface.xml", "yiew", 0);
//[...]
$.wrappers.MsgPlus.CreateWnd = function (XmlFile, WindowId, Options) {
    var toEval = "On" + WindowId + "Event_Cancel = function (plusWnd) { $.trigger(plusWnd, '" + WindowId + "Event_Cancel', plusQuery.$A(arguments)); };";
    toEval += "On" + WindowId + "Event_Destroyed = function (plusWnd, exitCode) { $.trigger(plusWnd, '" + WindowId + "Event_Destroyed', plusQuery.$A(arguments)); };";
    //etc
    eval(toEval);
    $(MsgPlus).LoadScriptFile("_blank.js"); // black magic happens here

    this.original.CreateWnd(XmlFile, WindowId, Options);
}
The loaded script file can contain anything. From my tests, it may not be completely empty though - one character will work though. I think the best options to fill this file with are a single space " " or a semicolon ";". You can even save it as ANSI and save a few bytes, Plus! will still parse it (although Plus! recommends Unicode).

I warned you, it's very hackish and not really recommended (eval is evil but so is LoadScriptFile) but unfortunately it's the only way I could get this to work. Feel free to use it if you think it's "good enough" but think about the consequences. If you decide to use this, don't forget to keep track of the created event handlers so you don't need to redeclare them when creating the same window twice. :P
RE: RE: [IDEA] plusQuery by Amec on 04-06-2011 at 04:11 PM

quote:
Originally posted by Matti
there's no global object in JScript
This is REALLY annoying... Surely they could switch to V8 or something? But then they'd have to write their own stuff for accessing ActiveX... Hmm.

quote:
Originally posted by Matti
Note that you don't need to trick eval() to be called in the global scope. By simply omitting the "var" keyword, the variable will be created in the global scope. It is generally a bad idea to omit the "var" keyword, but in this case it's sort of acceptable. :P
It's not really "tricking eval"... It's fine, and more clear than just declaring the variables without var. And what happens if there's a variable with the same name in the local scope? ;) (I know that'll almost never happen, but still)

Now that I think about it... Wouldn't there be a message to MessengerPlusLive_MsgPump we could send to have the script reevaluated? Not 0x81CE, (restarts all scripts) but something similar?
RE: [IDEA] plusQuery by matty on 04-06-2011 at 05:19 PM

One thing I noticed

js code:
    $.addEventListener("ChatWndReceiveMessage", function (chatWnd, origin, message, msgKind) {
        if (msgKind === 1)
        {
            $.triggerCommand($(chatWnd), $(chatWnd.Contacts).GetContact(origin), message.split(' ')[0].toUpperCase(), message.split(' ').slice(1).join(' '), message, true);
        }
    });
You cannot obtain the Contact object by passing the origin parameter to the GetContact function.
RE: [IDEA] plusQuery by Eljay on 04-06-2011 at 06:03 PM

JScript does have a global scope. In regular JScript (WSH) you can just do "var global = this" in global scope then access it anywhere.

It definitely worked at some point, but use of it was removed/broken by Plus! in an update to the scripting engine.
I'm sure there was a reason but I sure as hell can't remember it :P Maybe something to do with the event handling.

I know it worked because I made a script that allowed you to hook events or something like that, and my script broke :(

Edit: found it :P [Beta-ish Utility Release] Hook System
So it was broken at some point between 2006 and now, lol.


RE: [IDEA] plusQuery by Matti on 04-06-2011 at 06:11 PM

quote:
Originally posted by matty
One thing I noticed

You cannot obtain the Contact object by passing the origin parameter to the GetContact function.
He extended the GetContact implementation so that you can search by name as well.
js code:
$.wrappers.Contacts.prototype.extend({
    GetContact: function (str) {
        if (str.toLowerCase() === Messenger.MyEmail.toLowerCase()) {
            return this[0];
        } else if (this.original.GetContact(str)) {
            return $(this.original.GetContact(str));
        } else {
            for (var i = 0; i < this.length; i++) {
                if (this[i].Name === str) {
                    return this[i]; // Only returns the first match
                }
            }
        }

        return null;
    }
});
Still, it's not a good idea to do this. The value of origin may not resolve to the right contact, for example when a contact has a nickname assigned. Contact.Name always returns the name as published by the contact, whereas origin may contain the nickname of that contact. I think it's better to simply use origin - if the developer wants to resolve this to a contact, he can just implement this in the callback or override the event himself.



Also, the local command parsing should be a bit more sophisticated. For example, script commands should be escapable by adding an extra leading slash, such as "//mycommand". Also, parameters can be separated by many sorts of whitespace characters such as \n, \r,... Therefore, you'll need a regular expression to properly detect whether a given message is a script command and correctly separate the command from the parameter. Luckily, Cookie has written an excellent regular expression for this: CookieRevised's reply to Gettin data from "/" commands.

The way I parse local commands is based upon Cookie's regular expression but instead of using the global RegExp object to retrieve the data, I use the result from exec() itself. It's better to use your local data rather than having to rely on global side effects. ;)
js code:
var match = /^\/([^ \n\r\v\xA0\/][^ \n\r\v\xA0]*)(?:[ \n\r\v\xA0]([\s\S]*))?$/.exec(message);
if ( match ) {
    var command = match[1]; // note: command will have no leading slash
    var parameters = match[2];

    var tmp = $.triggerCommand($(chatWnd), $.MyContact, command, parameters, message, false);
    return (tmp === undefined ? message : tmp);
}
This principle can't be easily ported to external commands though, since you don't know what the leading character will be (! or @ or ...). However, you won't have to deal with escaped commands either. I'd recommend to simply look for the command until the first encountered whitespace character and use the remaining part as parameters.
js code:
var match = /^([^ \n\r\v\xA0]*)[ \n\r\v\xA0]?([\s\S]*)$/.exec(sMessage);
if ( match ) {
    var command = m[1]; // note: command will retain its leading character
    var parameters = m[2];

    $.triggerCommand($(chatWnd), $.MyContact, command, parameters, message, true);
}

quote:
Originally posted by Eljay
JScript does have a global scope. In regular JScript (WSH) you can just do "var global = this" in global scope then access it anywhere.
I know, I tried that but it's inaccessible. You can't read global variables from it or add variables to it. Heck, you can't even iterate over its contents. Very sad indeed. :(

Long post is long indeed.
RE: [IDEA] plusQuery by Eljay on 04-06-2011 at 06:20 PM

Ok I found when/why global scope was broken: Patchou's reply to 300 breaks NP script

It seems that when Patchou added the global enums (STATUS_BUSY, etc.), it broke the global scope :(


RE: [IDEA] plusQuery by CookieRevised on 04-06-2011 at 10:27 PM

Offtopic sidenote:

quote:
Originally posted by Matti
The way I parse local commands is based upon Cookie's regular expression but instead of using the global RegExp object to retrieve the data, I use the result from exec() itself. It's better to use your local data rather than having to rely on global side effects. ;)
It's not 'better' though (it isn't worse either for that matter). There is nothing wrong with using the global RegExp object as long as both functions (exec and the resulting global RegExp object) are used right after eachother. But if they are not used right after eachother, then yes, you have a very good point. But also, it is also not a 'side effect' though; the $1 identifiers are meant to be used like that.

But, this is just semantics I guess and what you prefer....


On topic, nice to see some advanced 'toying' (if I may use that word) with the scripting engine to create such a library and create such possebilities. But although I'm sure one could come up with an exotic use or examples for it, I must say, I can't see any real use of it in practice, certainly not for the average script and scripter, but also not for the more advanced scripts. But this said, again, great work (y).


RE: [IDEA] plusQuery by Amec on 04-07-2011 at 05:43 AM

quote:
Originally posted by Matti
Another thing I noticed is that you're extending Object.prototype.
Fix'd. Moved all of those functions to plusQuery.

quote:
Originally posted by Matti
Here's a suggestion: use combined getter/setter methods for the properties in the wrapper classes.
Done.

quote:
Originally posted by Matti
Also, the local command parsing should be a bit more sophisticated.
Aaaand done!

What I haven't done:
  • Implemented dynamic function binding for PlusWnd events. Not sure if I'm going to do this, yet... I'll have to test a lot before I decide.
  • Changed GetContact so you can't search by name. Haven't decided what I'm gonna do with this one... It's so simple with it there, but I'm aware of the pitfalls... Perhaps... ChatWnd.GetContactByName and ChatWnd.GetContactByEmail? Dunno.

quote:
Originally posted by Eljay
It seems that when Patchou added the global enums (STATUS_BUSY, etc.), it broke the global scope :(
Is he planning on fixing it? -_-

quote:
Originally posted by CookieRevised
On topic, nice to see some advanced 'toying' (if I may use that word) with the scripting engine to create such a library and create such possebilities. But although I'm sure one could come up with an exotic use or examples for it, I must say, I can't see any real use of it in practice, certainly not for the average script and scripter, but also not for the more advanced scripts. But this said, again, great work (y).
I find it useful for writing my own small scripts. Here's a recent example: I wanted to add two commands, "/afk" and "/back", which set my name to Messenger.MyName + "\xA0" + status, and back again. Pretty simple, ey? So instead of creating a whole new script for it, I just created a new .js in my "Misc" script which has _plusquery.js, and added it using $.addCommand. Much easier/faster/better(?) than creating a whole new script and doing all the parsing of commands, etc. again.
RE: [IDEA] plusQuery by CookieRevised on 04-07-2011 at 09:36 AM

Sure, but as for that specific example, that could be done in much easier ways too though I think (as in using a more straightforward library). So, maybe not the best example. But as long as you (and others) find it useful, then (y)