VS Code, Typescript – Bare Metal New Project

i’m basically following this guide, while humbly attempting to trim down to bare necessity and re-align configs with crucial bits that’ve shifted since then… and likely to continue shifting 😐

  1. install Node… there’s many ways but their setup.exe is handy… this includes npm
  2. from cmd.exe: npm install -g typescript (-g means globally vs project specific)
  3. install VS Code via setup.exe
  4. launch VS Code… commands inside VS Code designated as “vsc>” from here-on
  5. vsc> File > Open > New Folder > “projectFolder” > then Open that folder
  6. vsc> F1 > type “task” > “Configure Task Runner” > Enter > “TypeScript – Watch Mode” > Enter
    1. this will create a crucial tasks.json file with working default settings…
    2. -AND- that “watch mode” choice means the moment you save any .ts file, the IDE will automatically regen the corresponding .js files… which plays into live edit and continue style debugging
  7. vsc> File > New File > populate with the following json block and save as tsconfig.json … this directs vscode to “transpile” .ts script to standard .js for us
    {
        "compilerOptions": {
            "target": "es5", 
            "outDir": "out/",
            "sourceMap": true
        }    
    }
  1. vsc> File > New File > throw in something simple like console.log("Hello World!"); and save as app.ts
  2. vsc> build aka compile via CTRL+SHIFT+B… after a few pregnant seconds, this will gen some stuff in the out folder that we specified in above tsconfig.json
  3. vsc> CTRL+SHIFT+D to get into Debug panel > click the gear icon which creates and opens default launch.json which should have working defaults going by what we’ve done previously
  4. crucial and subtle, navigate back to the app.ts file as the active tab you wish to run/debug (this corresponds with the relative reference, "program": "${file}", in launch.json)
  5. now we should be able to simply hit F5 to run/debug from here-on as we’d normally expect… F9 to set breakpoints, etc.

hopefully you’re off to the races and you can bootstrap yourself further by googling

i am a bit “ashamed” this is still so obtuse

Syncfusion Xamarin.Forms SfDataGrid – Binding to List<Dictionary<,>>

corresponding syncfusion forums post

Binding to Collection<Dictionary<,>> can be accomplished but doesn’t appear to be well documented… this WPF SfDataGrid doc gave the clue.

The basic trick is to set the SfDataGrid’s column.MappingName = “Fields[FieldName]”;
where Fields is a Dictionary property on your DictionaryWrapper class.

I couldn’t get List<Dictionary<string, object>> working directly without the wrapper class “hiding” the dictionary from what I think is an SfDataGrid bug. The app crash exception call stack ultimately winds up on an invalid Linq related get_Item() call.

Below is sample working code including “SimpleTable” wrapper for List and Newtonsoft type converter for deserializing Json “table” directly into this datastructure… this facilitates delivery of tabular resultsets from web api’s… see my SqlClientHelpers.ToJson method as one implementation that works succinctly within Azure Functions for example.

FYI, I believe there is also a bug with SfDataGrid column sorting logic when bound to this kind of Dictionary, a non-fatal exception fires. I worked around by implementing grid.SortColumnsChanging.

Sample Deserialize call:

var data = "[{\"Source\":\"Web\",\"Batch Id\":1}, {\"Source\":\"Manual\",\"Batch Id\":2}]";
var table = JsonConvert.DeserializeObject<SimpleTable>(data);

Binding sample with crucial MappingName syntax:

grid.ItemsSource = table;
grid.Columns.Add(new GridTextColumn()
{
  HeaderText = "Source",
  MappingName = "Vals[Source]" // **** HERE'S THE KICKER ****
});

SimpleTable.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace DataHelpers
{
  [JsonConverter(typeof(DictRow_DictDeserializer))]
  public class DictRow
  {
    public Dictionary<string, object> Vals { get; set; }
    public DictRow(Dictionary<string, object> dict) { Vals = dict; }
  }

  public class SimpleTable : List<DictRow>
  {
    public SimpleTable(IEnumerable<DictRow> list) : base(list) { }
  }

  public class DictRow_DictDeserializer : JsonConverter
  {
    public override bool CanRead => true;
    public override bool CanWrite => false;
    public override bool CanConvert(Type objectType) => objectType == typeof(Dictionary<string, object>);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
      return new DictRow(serializer.Deserialize<Dictionary<string, object>>(reader));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
      throw new NotSupportedException();
    }
  }
}

ASP.Net Controller API nuggets

Write Directly to Response Stream

Stack-o basically covers this already but kinda danced around how minimal the code can really be for the basic scenario…

//ignore this...
//e.g. $.ajax(url, { procName: "", parms: {deliveryDate:"3/2/2016"}, returnParmName: "" }, ...)
public class GenericProcArgs
{
  public string procName { get; set; }
  //from: http://stackoverflow.com/questions/5022958/passing-dynamic-json-object-to-c-sharp-mvc-controller
  //super convenient way to receive JS object with arbitrary properties to be fed straight to proc parms
  public dynamic parms { get; set; }
  /// <summary>
  /// whether to titleCase the JSON property names (handy for feeding auto built datagrids)
  /// </summary>
  public bool titleCase { get; set; } = false;
  public string returnParmName { get; set; }
}

//here's the beef...
public ActionResult GetProc(GenericProcArgs args)
{
  Response.ContentType = "application/json";

  using (var proc = new Proc(args.procName))
  {
    proc
      .AssignParms(args.parms as IDictionary<string, object>)
      .ExecuteJson(Response.OutputStream, args.titleCase);
  }
  return new EmptyResult();
}

  • in my experience, the ContentType wasn’t even necessary but it feels like good practice
  • here i’m using a database layer represented by my own custom Proc class… which is a bundle of convenient methods wrapped around SqlCommand … in this case, it’s firing SqlCommand.ExecuteReader() and writing the results directly to the response via Newtonsoft.Json StreamWriters… i honestly haven’t done a real profile on this call stack but it feels like a nice straight shot from sql streaming through web tier transform to client
  • another nifty nugget in play here is the usage of C# Dynamic to catch arbitrary JS objects posted from the ajax client, and that Dynamic is then inherently castable to a Dictionary

Kendo UI Nuggets

Grid doesn’t like spaces in dataSource field names

(I happen to be subscribing to KnockoutJS observable here to receive data feed but that naturally depends on your framework of choice.)
Typical Kendo JS error: invalid template
Particularly note the way the columns property is generated.
[crayon toolbar=”always” lang=”js”]
sigCapVm.packingSlips.subscribe(function (newval) {
var gridPackingSlips = $(“#gridPackingSlips”);
var kgrid = gridPackingSlips.data(“kendoGrid”);
if (kgrid && kgrid.columns.length) kgrid.dataSource.data(newval);
else {
if (kgrid) kgrid.destroy();
gridPackingSlips.kendoGrid({
//workaround for autogenerating columns with spaces
//from: http://www.telerik.com/forums/field-name-with-space-and-other-than-numeric-creating-issues-to-load-the-grid#23fk8zzWZkioYCn7Tr4xFg
columns: $.map(Object.keys(newval.length === 0 ? {} : newval[0]),
function (el) {
if (el.startsWith(““)) return; //by convention, don’t show fields starting with “
return { field: ‘[“‘ + el.replace(“#”, “\#”) + ‘”]’, title: el }
}), //hash aka pound (#) is special character in kendo template syntax
dataSource: { data: newval },
noRecords: { template: “no records found for those inputs” },
height: “20em”,
change: function (e) {
//good to remember that we’re not doing a true knockout binding here vs setting the grid’s dataSource.data property above
//so we have to “manually” connect the selected row back to the viewmodel here
var selectedPackingSlip = e.sender.dataItem(e.sender.select());
var vm = ko.contextFor(gridPackingSlips[0]).$root;
vm.selectPackingSlipId(selectedPackingSlip._PackingSlipId);
},
resizable: true,
//scrollable: false,
sortable: true,
filterable: false,
selectable: true
});
};
});
[/crayon]

Nuget Cheatsheet

tip command line
uninstall all packages matching pattern get-package | ? {$_.Id -like "Human*" } | % { uninstall-package $_.id } 1

  1. speaking of Humanzier, just install Humanizer.Core to get English only 

JavaScript (ES7) async wrappers perfect for Chrome.Bookmarks API

Background:

I tasked myself with a little project to sync my Chrome Bookmarks Bar with my Diigo links.

image

Chrome’s Bookmarks API is very straightforward but it is a traditional async callback style call… so it became a good opportunity for me to cozy up to EcmaScript v7’s (ES7) async/await support… since this is done in a Chrome Extension, it lends itself to this hard dependency on ES7 syntax…

async/await is supported as of Chrome v55 in Dec.2016

to me, the basic gist of this shift to async away from traditional callbacks means that we get to flatten all those nested callbacks like this:

//
call_method1(function(response1){
  call_method2(function(response2){
  });
});

to this:

//
let response1 = await call_method1();
let response2 = await call_method2();

admittedly, maybe not super dramatic when you boil it down like that but it really feels like previously complex nested code is easier to read/maintain and modularize when this is now available

Great async reference articles:

Code Nugget:

I want to elaborate further on the very happily working Diigo solution, but for now just the basic async bits…

//
//here's the wrapper
//one main thing to be aware of is that the native Promise object is what we can "await" on
function createBookmarkAsync(parms) {
  return new Promise(function(fulfill, reject) {
    chrome.bookmarks.create(parms, fulfill);
  });
}

//simple usage of the above wrapper
$('#btnRefreshDiigo').click(async function() { //CRITICAL = note the "async" before the function declaration... otherwise google unhelpfully reports that await is an undeclared identifier
  let diigoFolderId = (await createBookmarkAsync({parentId: "1", title: "Diigos"})).id;
  //now continue coding knowing that this code wont execute until the async call returns, very cool!!
}

Also handy for Fetch() API:

it was also great to use the very nicely straightforward new’ish Fetch API (Chrome v43+) and await on that response vs something like jquery.ajax

//
let response = await fetch(json_api_url, { credentials: 'include' }); //the credentials bit passes existing cookies (e.g. leveraging existing login)
let jsonText = await response.text();
let items = JSON.parse(jsonText).items;

Automatic Header Links

I envied this little eye candy on some other blogs and it was easy to throw together.
 
image

If you’re on WordPress, a plugin like Simple Custom CSS and JS comes in handy for stuff like this.

CSS

a.headerLink:hover {
  color: inherit;
  opacity: 1;
}
a.headerLink {
  position: relative;
  transition: opacity 0.5s ease-in-out;
  cursor: pointer;
  opacity: .2;
}
a.headerLink i.fa {
  position: absolute;
  left: -1.5em;
}
a.headerLink:focus {
  color: inherit !important;
}

JS

  $(':header:not("[class]")').each(function(idx, el){
    var l = $(el);
    var name = l.text().replace(/\W+/g, "");
    l.prepend('<a name="'+name+'" class="headerLink" href=#'+name+'><i class="fa fa-chain"></i></a>');
  });

Exposing Azure Function web API to native & web clients through Azure AD authentication

Problem

Azure AD writeups are prevalent but I was really struggling to find examples of calling the same Azure Function API, secured by Azure AD Authentication, by both Native as well as Web clients (since we can only select one app type in the Azure AD App registration, not both).

TL;DR

The kicker solution for me was having both a web and native App registration (i.e. two Client Id’s) and providing the WEB App registration’s Application Id as the “RESOURCE” parameter to the AuthenticationContext.AcquireTokenAsync() call in the Native app (see code sample below).

So the web registration is tied directly to the Azure Function… and then we’re piggybacking the web registration by requesting the web as the resource parameter in the native client call … i haven’t seen this documented yet so i can’t say whether this is an officially preferred solution.

Basic Steps

This is a good getting started guide guide, in parity with current landscape.

  1. get your Azure Function working as a web api… probably doesn’t matter whether web or native comes first but it seems like the web is more “trusted” from an OAuth standpoint and more clearly documented… OAuth refers to native clients as “public” and requiring a couple more OAuth contortions than web clients.
  2. create a Web type entry for your Function under New Portal > Azure Active Directory > App registrations… all the defaults are good, except you’ll need to create the Reply URLs that are valid for you… reply url is a parameter to your ADAL.js client call… in the end this entry provides the crucial Application Id aka Client Id
  3. now configure this web registration for AD Auth via New Portal > App Services > {your Function app} > Function app settings > Configure authentication > Authentication Providers > Azure AD > Express >
    1. Azure AD App = the Web App registration name you gave above
  4. Now create another Azure AD > App registration as Native type and (HERE’S THE KICKER) > Settings > Required Permissions > Add > Select an API > TYPE IN YOUR web App registration name in the search box and it’ll show up to be selected
  5. finally, use the Application Id guid from your web app as the RESOURCE parameter to the AcquireTokenAsync() call in your native app

Working ADAL.js web client code sample

function adalResultHandler(err, token) {
  if (err) {
    self.userName(null);
    lobibox.notify("error", { size: "mini", title: "Azure AD Auth", msg: err });
    return false;
  } else {
    self.userName(adal.getCachedUser().profile.name);
    //lobibox.notify("info", { size: "mini", msg: "login successful\nuser: " + user.profile.name + "\ntoken: " + token });
    return true;
  }
}

var adal = new AuthenticationContext({
  instance: "https://login.microsoftonline.com/",
  tenant: "{your domain}.onmicrosoft.com",
  clientId: "{your web guid}", // your Azure AD > App registrationS > {your web api} > APPLICATION ID
  //NUGGET: these "reply URLs" are set under Azure Portal > AD > App registrations > {your App Service} > Settings > Reply URLs
  //NOT under {your App Service} > Settings > (Manage) Auth > AD > Redirect URLs !!!
  redirectUri: window.location.href, //REPLACE WITH YOUR REDIRECT URL
  popUp: true
});

adal.callback = function (err, token) { if (adalResultHandler(err, token)) doSomething(); }

adal.login();

Working Xamarin Native iOS app client code sample

private const string Instance = "https://login.microsoftonline.com";
private const string Tenant = "{your domain}.onmicrosoft.com"; //common //COMMON OR YOUR TENANT ID // "hfcazure.com", //"4be68759-0968-4760-b716-f82711a28fcb", //http://stackoverflow.com/questions/26384034/how-to-get-the-azure-account-tenant-id
private const string ClientId = "{your native guid}"; //from your Azure AD > App registrations > {your ***NATIVE*** api} > APPLICATION ID
private const string RedirectUri = "https://{your azure function api name}.azurewebsites.net";
private const string ResourceId = "{your web guid}"; //take this from your Azure AD > App registrations > {your ***WEB*** api} > APPLICATION ID // **isn't that interesting, we're requesting another API as the "resource" of this api**

var Azure_OAuth2_Authority_Url = $"{Instance}/{Tenant}/oauth2/authorize");
var authContext = new AuthenticationContext(Azure_OAuth2_Authority_Url);

var authResult = await authContext.AcquireTokenAsync(ResourceId, ClientId, new Uri(RedirectUri), await _platformParameters.GetAsync()); //_platformParameters is something i whipped up special
CurrentUser = new HfcUserAuth
{
  FirstName = authResult.UserInfo?.GivenName,
  LastName = authResult.UserInfo?.FamilyName,
  AccessToken = authResult.AccessToken,
  IdToken = authResult.IdToken
};

Typical error responses

Various attempts at sussing out a valid resource value for the AcquireTokenAsync() in my Xamarin Forms native iOS app would yield the following error:
AADSTS65005: The client application has requested access to resource <xyz>. This request has failed because the client has not specified this resource in its requiredResourceAccess list

i was also getting these where {app} was the resource i was passing when i had the ClientId parameter wrong
AADSTS50001: The application named {app} was not found in the tenant named {tentant}.

Helpful references

What is my Tenant Id or “Authority” URL ???

Wanted to mention this in closing since “Tentant” is currently so ambiguously referred to in the documentation i ran into…
New Portal > Azure Active Directory > App registrations > Endpoints is where you pull the “Authority” Url from the “OAUTH 2.0 AUTHORIZATION ENDPOINT” slot – the main argument for new AuthenticationContext()

for example:
https://login.windows.net/9198d419-6ce5-4229-a457-8c38421f7466/oauth2/authorize
this “9198…” guid is your Tenant Id (don’t worry this one is made up)

our tenant appears to be simply our azure ad domain name, at least in typical configurations… so this works here as well:
https://login.windows.net/XYZ.onmicrosoft.com/oauth2/authorize

image

GreaseMonkey hacking Gmail

motivation

i wanted to see if i could get category bundling working in gmail ala outlook… it’s always been a nice mental flow for me to carve out pending events from littering the “inbox zero” but still see them right there in front so i don’t forget to check up on them vs hidden a click away under a “folder”
 

update – don’t miss the “labs” functionality for doing exactly this kind of category bundling
 

success!

the integration with gmail’s normal behavior isn’t perfect so this is still very experimental stages but it’s a pretty satisfying quick hack…
 

notes

  • i followed a this tutorial for getting the gmail API cooking including OAUTH2…
  • the greasemonkey end of the code has some helper functions to get all the usual libraries loaded up like bootstrap, font-awesome and knockout… currently i’m only leveraging jQuery and Lobibox (love those sexy growls!)
  • tweak the code to call gapi.client.gmail.users.labels.list vs messages to see your label Id’s… they’re not the names we see in the gmail UI

roadmap

obvious next nice to haves rush to mind:

  • bundle by multiple specified categories (aka labels)
  • clicking on message in new section actually does navigate to message since that was an easy gimme but it’s not as slick in the vertical split mode… i tried tracing the code that dynamically populates the side panel with the message body and it’s just to abstract, so it’ll have to be a matter of popping that in myself, but shouldn’t be too tough since we’re already loading the whole message body behind the scenes.
  • flip to real ECMAScript 2016 module loader or whatever’s clever right now… without a build engine to make things happen for script.js it seems like we’re still waiting for a fully native solution… i could do require.js approach but that seems to be on the outs already???

example screenshot

image
 

GmailMonkey.user.js

// ==UserScript==
// @name         GmailMonkey
// @namespace    http://Next-Technologies.com/
// @version      0.1
// @description  try to take over the world!
// @author       Brent Anderson
// @match        https://mail.google.com/mail/u/0/*
// @grant        none
// @run-at        document-end
// ==/UserScript==


//gmail api stuff//////////////////////////////////////////////////////////////////////////

function objArrayGetByProperty(array, propertyName, value) {
  var result = $.grep(array, function(e){ return e[propertyName] === value; });
  return result.length ? result[0].value : null;
}

function loadMessages() {
  var request = gapi.client.gmail.users.messages.list({
    "userId": "me", //nugget: special userId of me to indicate the currently authenticated user
    "labelIds": "Label_59",
    "maxResults": 200
  });

  request.execute(function(response) {
    if (response.error) {
      Lobibox.notify("error", { size: "mini", delay: false, msg: "[gapi.client.gmail.users.messages.list] " + response.error.message });
      return;
    }

    $.each(response.messages, function() {
      var messageRequest = gapi.client.gmail.users.messages.get({
        "userId": "me",
        "id": this.id
      });

      messageRequest.execute(function (message) {
        //debugger;
        var from = objArrayGetByProperty(message.payload.headers, "name", "From").replace(/"/g,"");
        var subj = objArrayGetByProperty(message.payload.headers, "name", "Subject");
        var date = formatDate(new Date(objArrayGetByProperty(message.payload.headers, "name", "Date")), "MMM DD");

        var onclick = "window.location.href += '/"+message.id+"'";

        mainTable.append(verticalSplit ? '\
          <tr class="zA yO apv" onclick="'+onclick+'">\
          <td colspan="2" rowspan="3"></td>\
          <td class="yX xY apy">'+from+'</td>\
          <td class="yf xY apt">'+date+'</td>\
          <td class="xY" rowspan="3"></td>\
          </tr>\
          <tr class="zA yO apv" onclick="'+onclick+'">\
          <td colspan="2" class="xY apD" style="font-weight: bold">'+subj+'</td>\
          </tr>\
          <tr class="zA yO apv apw" onclick="'+onclick+'">\
          <td colspan="2" class="xY apA apB y2">'+message.snippet+'</td>\
          </tr>\
          ': '\
          <tr class="zA yO" onclick="window.location.href += \'/'+message.id+'\'">\
          <td colspan="4"></td>\
          <td class="xY">'+from+'</td>\
          <td colspan="2" class="xY" style="overflow: hidden">'+subj+'</td>\
          <td class="xW xY">'+date+'</td>\
          </tr>'
        );
      });
    });
  });
}

//from here: https://www.sitepoint.com/mastering-your-inbox-with-gmail-javascript-api/
function handleAuthClick() {
  gapi.auth.authorize({
    client_id: clientId,
    scope: scopes,
    immediate: false
  }, function (authResult) {
    if(authResult && !authResult.error) {
      gapi.client.load("gmail", "v1", loadMessages); // <<<<<<<<<<<<<<<<<<<<<<<
      /*$("#authorize-button").remove();
      $(".table-inbox").removeClass("hidden");
    } else {
      $("#authorize-button").removeClass("hidden");
      $("#authorize-button").on("click", handleAuthClick);
      */
    }
  });
  return false;
}

function nextTechInit() {
  waitForIt(":2", function(found) {
    $("html, body").css("font-size", "inherit"); //override bootstrap's annoying default

    $(".vh").remove();

    Lobibox.notify.DEFAULTS.soundPath = "//cdn.rawgit.com/arboshiki/lobibox/373d1af467930db68c876e76408bd953198c428e/dist/sounds/";
    Lobibox.notify.DEFAULTS.delay = 3000;
    //Lobibox.notify("error", { size: "mini", delay: false, msg: "lobibox test" });

    mainTable = $(".Cp > div > table");
    verticalSplit = mainTable.find("colgroup > col").length === 5;

    mainTable = $(mainTable).find("tbody");
    mainTable.append(
      '<tr><td colspan="'+(verticalSplit?5:8)+'"><div style="margin-top: 1em" class="">Pending</div></td></tr>');

    gapi.client.setApiKey(apiKey);
    handleAuthClick();
  });
}

function formatDate(date, format) {
  var monthNames = [
    "January", "February", "March",
    "April", "May", "June", "July",
    "August", "September", "October",
    "November", "December"
  ];

  var day = date.getDate();
  var monthIndex = date.getMonth();
  var year = date.getFullYear();

  return format.replace("MMM", monthNames[monthIndex].slice(0,3)).replace("DD", day);
}

//////////////////////////////////////////////////////////////////////////////////

function waitForIt(elementId, then) {
  //console.log("waitForIt: " +elementId);
  var found = document.getElementById(":2");
  if ( !found ) {
    //console.log("not found!"); 
    setTimeout(function() {waitForIt(elementId, then);}, 500);
    return;
  }
  then(found);
}

function loader(refsArray) {
  var element;
  for(var i = 0; i < refsArray.length; i++) {
    url = refsArray[i].toString();

    if (url.indexOf(".js") !== -1 || url.slice(0,8) === "function") {
      element = document.createElement("script");
      if (url.slice(0,8) === "function") { 
        element.innerHTML = url;
        document.getElementsByTagName("head")[0].appendChild(element);
        continue; //nugget! inline script doesn't fire an onload event
      }
      else element.src = url;
      if (i < refsArray.length-1) { //if we're not on the last element already, recurse on the remaining items
        var remaining = refsArray.slice(i+1);
        element.onload = function() {
          loader(remaining);
        };
      }
      document.getElementsByTagName("head")[0].appendChild(element);
      break;
    }

    else if (url.indexOf(".css") !== -1) {
      element = document.createElement("link");
      element.rel = "stylesheet";
      element.type = "text/css";
      element.href = url;
      document.getElementsByTagName("head")[0].appendChild(element);
    }

    else throw("unexpected reference extension, expecting .css or .js or a function, but got: "+url);

  }
}

//by simple convention, list all CSS first, then JS...
//each JS will subsequently load the next as a simple dependency mechanism so specify JS's in appropriate order
//further, including a function(s) will inline <script> it
loader([
  //"//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css",
  //"//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css",
  //"//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css",
  "//cdn.rawgit.com/arboshiki/lobibox/master/dist/css/lobibox.min.css",
  "//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.15.1/jquery.validate.min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js",
  "//cdn.rawgit.com/arboshiki/lobibox/master/dist/js/lobibox.min.js",
  objArrayGetByProperty.toString()+"\r\n"+
  'var mainTable;\r\n'+
  'var verticalSplit = false;\r\n'+
  'var clientId = "xxxxxxxxxxxxx.apps.googleusercontent.com";\r\n'+
  'var apiKey = "xxxxx";\r\n'+
  'var scopes = "https://www.googleapis.com/auth/gmail.readonly";\r\n\r\n'+
  loadMessages.toString()+"\r\n"+
  handleAuthClick.toString()+"\r\n"+
  waitForIt.toString()+"\r\n"+
  nextTechInit.toString()+"\r\n"+
  formatDate.toString()+"\r\n",
  "//apis.google.com/js/client.js?onload=nextTechInit"
]);

[Solved] Greasemonkey/Tampermonkey jQuery sideload and setInterval

i was having a heck of a time keeping a reliable handle on jQuery in the Pandora page… it would be there upon initial Greasemonkey script execution but then upon subsequent setInterval executions, the jQuery global variable was undefined… fascinating…
 

notable: as i was debugging, i started to see that Chrome was cycling through four ( 4 ! ) different VMxxxx “copies” of the greasemonkey script upon each setInterval execution… questions like why? and why 4? abound if anyone cares to enlighten me
 

so it struck me that i just need to make sure jQuery is available in each one of those “sessions”…
 

noteable: the “sideload” is accomplished via jQuery’s native “noConflict” facility… this post explains how it works… the gist is that each load of jQuery does indeed replace “$” BUT it also saves the previous into _$, such that $.noConflict can restore “$” to the previous version… this is what allows Pandora’s copy of jQuery to remain as-is… crucial in this case because Pandora depends on additional add-ons that it loads as expando properties on its instance of jQuery.
 

after that was in the bag, i couldn’t help dwelling on what else might be possible and had another aha moment… from tracing the pandora js execution i learned that there were pretty obvious variables getting set for allowed features (e.g. “allowSkipTrackWithoutLimit”)… i banged around quite a bit trying to replace the main pandora.js script with one where those values were tweaked… blocking the original script via AdBlockPlus was easy as well as loading the tweaked pandora.js inline <script> but that approach ran aground on not being able to load other dependency scripts in proper sequence with the replacement… Chrome doesn’t implement the crucial window.beforescriptexecute event which would probably make this feasible… the main pandora.js is wrappered in a self contained function call so we can’t monkey patch its innards…

but then it struck me, jQuery is global… and what if they’re getting these values via jQuery.ajax… such that i could override and tweak… sure enough, that approach panned all the way out!
 

update – after that last round, i realized the whole thing about sideloading jQuery was unnecessary, i just needed to use the inline script approach to make sure my code executed on the page context vs whatever weird context TamperMonkey normally does… so the following script now reflects the cleaner evolved approach

// ==UserScript==
// @name          Pandora - "still listening" click
// @author        Brent Anderson
// @homepage      /2016/08/solved-greasemonkey-jquery-sideload-and-setinterval.html
// @match         http://www.pandora.com/*
// @grants        none
// @run-at        document-end
// ==/UserScript==

function recurringTweaks() {
  //this click, remove, click sequence skips embedded video ads and gets the tunes playing again
  var stillListeningButton = $("#still_listening_ignore");
  if (stillListeningButton.is(":visible")) {
    stillListeningButton.click();
    $("#videoPlayerContainer").remove();
    stillListeningButton.click();
    $(".playButton").click();
    //above brute force video ad skip leaves player controls disabled, this resolves that side effect
    $(".disabled").removeClass("disabled");
  }

  var adContainer = $("#ad_container");
  if (adContainer.length) {
    //remove right side ad section...
    $("#ad_container").remove();
    //and allow the album covers area to fill the space
    $(".contentContainer").css("width", "100%");
    $("#adLayout").css("width", "80%");

    //remove some other "upgrade" bits
    $(".registeredUser").remove();
    $("#rightColumnDivider").remove();
    $(".audioAdInfo").remove();
  }
}


// monkey patch jQuery.ajax so we can override some nice stuff =)
var hijax = function() {
  if (typeof $ !== 'undefined') {
    var oldAjax = $.ajax;
    var newAjax = function(a, b) {
      var oldSuccess = a.success;
      a.success = function(data, textStatus, jqHXR) {

        // infinite skip! =)
        $(data).find('name:contains(allowSkipTrackWithoutLimit) + value > boolean').replaceWith('<boolean>1</boolean>');

        //auto skip ads
        if (a.url.indexOf("method=registerImpression") !== -1) {
          $(".skipButton a").click();
        }

        //debug: console.log('url: ' + a.url + ', data: '+(''+data === '[object XMLDocument]' ? data.children[0].innerHTML : data));
        oldSuccess(data, textStatus, jqHXR);
      };
      oldAjax(a, b);
    };
    $.ajax = newAjax;

    setInterval(recurringTweaks, 2000);

  }

};

// load <script> inline to the page so it has access to jQuery "$" global vs TamperMonkey's alternative context
if (!document.getElementById("hijax")) {
  var hijaxScript = document.createElement("script");
  hijaxScript.setAttribute("id", "hijax");
  hijaxScript.innerHTML = recurringTweaks.toString() + "\r\n" + hijax.toString().replace(/^function.*{|}$/g, "");
  document.head.appendChild(hijaxScript);
}



///////////////////////////////////////////////////////////////////////////////////////////////////////
//sorry, turning this post into a catch all for stuff that might come in handy elsewhere
/*

//the original jquery "sideload" code
function loadJq() {
    if (!window.jq) {
        script = document.createElement("script");
        script.src = "http://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js";
        script.onload = function() { window.jq = $.noConflict(true); cosmetics(); };
        document.getElementsByTagName("head")[0].appendChild(script);
    }
    else cosmetics();
}

    //helpful: http://userscripts-mirror.org/scripts/show/125936

  window.addEventListener('beforescriptexecute', function(e) {
      if (e.target.src.indexOf("pandora.js") != -1) {
          e.preventDefault();
          //e.stopPropagation(); //??
          e.target.src = ''; //??
          e.target.innerHTML = "patched script";
      }
  }, true);

var a = document.getElementsByTagName("script");
for each (var e in a) {
  if (!e) continue; // oddly, this does sometimes grab null elements.
  var b = e.getAttribute("src");
  if (b && b.indexOf("pandora.js") != -1) {
    e.parentNode.removeChild(e);
    debugger;
    break;
  }
}
*/

// @grants         GM_xmlhttpRequest
/*GM_xmlhttpRequest({
  method: "GET",
  url: "http://rawgit.com/Beej126/567a36f2dd1e3ce613ad8ec5846a40d4/raw/fac20b4ab17681b5da41b07c2549676ff3571fc9/dorPanda.js", //"http://www.pandora.com/pandora.js?v=440211416",
  onload: function(response) {
    debugger;

    //here's the beef!
    //var tweaked = response.responseText.replace("this.PC=b.allowSkipTrackWithoutLimit", "this.PC = true;");
    //$("script[src*='/pandora.js'").af

    var tweaked = response.responseText;
    document.head.appendChild(document.createElement('script')).innerHTML = tweaked;
  }
});*

*/

starting the same hijinx for Spotify… they load MooTools into $ and for some reason the selector wasn’t finding obvious classes… i’ve never picked up MooTools so maybe the syntax is different than jQuery… so i just went back to the jQuery sideload approach on this one… after that, worked it down into pure DOM, no jQuery needed

// ==UserScript==
// @name          Spotify tweaks
// @author        Brent Anderson
// homepage      /2016/08/solved-greasemonkey-jquery-sideload-and-setinterval.html
// @match         https://play.spotify.com/*
// @grants        none
// @run-at        document-end
// ==/UserScript==

function terminator() {
  var target = document.getElementsByClassName("ads-leaderboard-container");
  if (target.length) {
    console.log("bye bye =)");
    target[0].remove();
    clearInterval(timerId); //kill the timer once the targeted element finally shows up
  }
}

//replace main.js with hacked version
//(block original with AdBlockPlus plugin)
//was easy to enable "next" button during ads but it sticks to the ad anyway, would take further effort and not worth it until they actually fire enough ads to be annoying
var scripts = document.getElementsByTagName("script");
for(var i = 0; i<scripts.length; i++) { if(scripts[i].src.indexOf("https://play.spotify.edgekey.net/apps/player/4.2.0/main.js") != -1) {
  //debugger;
  var mainjs = document.createElement("script");
  mainjs.crossorigin = "anonymous";
  mainjs.src = "https://rawgit.com/Beej126/1501d5acb4fd20a6fcdcfe6599ce0c5e/raw/2725727f297a00444ef51c490a6009458a513e07/SpotifyMain.js";
  document.body.appendChild(mainjs);
  break;
}}

//there were multiple iframes, targeting the one that actually gets the ads
if (document.body && document.body.classList.length && document.body.classList[0] === "non-mobile" && document.body.attributes.length === 1) {
  //setup a recurring check to see when ads get dynamically inserted into page
  var script = document.createElement("script");
  script.innerHTML = terminator.toString() + "\r\n" + "var timerId = setInterval(terminator, 2000);";
  document.head.appendChild(script);
}