├── LICENSE ├── README.markdown └── appengine ├── app.yaml ├── html ├── callback.html ├── index.html └── xdr.html ├── images ├── minibutton_icons.png └── minibutton_matrix.png ├── javascript ├── easyXDM.min.js └── github-anywhere.js ├── main.py └── style └── style.css /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Abraham Williams - @[abraham](https://twitter.com/abraham) - - 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | GitHub Anywhere 2 | =============== 3 | 4 | An experimental JavaScript platform for adding GitHub follow, watch, and fork widgets to any site with a simple HTML snippet. You can try out a demo on . 5 | 6 | Warning 7 | ------- 8 | 9 | GHA is alpha quality. It may break things and could have security vulnerabilities. Use at your own risk. 10 | 11 | Using GHA 12 | --------- 13 | 14 | To add GHA to your site add the following snippet inside of the `` section of your page. 15 | 16 | 17 | 22 | 23 | Adding the following links to your page will create a user follow button and a watch repo button. Be sure to replace `:user` with the GitHub username and `:repo` with the repository name. 24 | 25 | Follow :name on GitHub 26 | Watch :name/:repo on GitHub 27 | 28 | For example a follow button for `abraham` would look like this: 29 | 30 | Follow abraham on GitHub 31 | 32 | Support 33 | ------- 34 | 35 | If you find bugs or have any feature requests please [let us know](https://github.com/abraham/github-anywhere/issues). 36 | 37 | Browser support 38 | --------------- 39 | 40 | While GHA is primarily tested in Google Chrome it should work in any modern browser with localStorage support. 41 | 42 | Hacking 43 | ------- 44 | 45 | Interested in hosting your own version or participating in the development? The code is hosted on [GitHub](https://github.com/abraham/github-anywhere) under an MIT license. Feel free to fork, hack, and generally tear apart the code. Be sure to jump into the [Google Group](https://groups.google.com/forum/#!forum/github-anywhere) and say hi! 46 | 47 | Credits 48 | ------- 49 | 50 | The proxy for the [GitHub API](http://develop.github.com/) is hosted on [Google App Engine](http://code.google.com/appengine/). The platform uses [easyXDM](http://easyxdm.net/), [jQuery](http://jquery.com/) and [David Walsh's GitHub buttons](http://davidwalsh.name/github-css). 51 | 52 | Authors 53 | ------- 54 | 55 | GHA is maintained by: 56 | 57 | Abraham Williams - @[abraham](https://twitter.com/abraham) - - 58 | 59 | Ryan LeFevre - @[meltingice](https://twitter.com/meltingice) - - -------------------------------------------------------------------------------- /appengine/app.yaml: -------------------------------------------------------------------------------- 1 | application: githubanywhere 2 | version: dev 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /xdr.html 8 | static_files: html/xdr.html 9 | upload: html/xdr.html 10 | secure: always 11 | 12 | - url: /callback.html 13 | static_files: html/callback.html 14 | upload: html/callback.html 15 | secure: always 16 | 17 | - url: /github-anywhere.js 18 | static_files: javascript/github-anywhere.js 19 | upload: javascript/github-anywhere.js 20 | secure: always 21 | 22 | - url: /javascript 23 | static_dir: javascript 24 | secure: always 25 | 26 | - url: /images 27 | static_dir: images 28 | secure: always 29 | 30 | - url: /style 31 | static_dir: style 32 | secure: always 33 | 34 | - url: /user/.* 35 | script: main.py 36 | secure: always 37 | 38 | - url: /repos/.* 39 | script: main.py 40 | secure: always 41 | 42 | - url: /login/.* 43 | script: main.py 44 | secure: always 45 | 46 | - url: .* 47 | static_files: html/index.html 48 | upload: html/index.html 49 | secure: always -------------------------------------------------------------------------------- /appengine/html/callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Callback - GitHub Anywhere 6 | 7 | 8 | 31 | 32 | 33 |
34 |

GitHub Anywhere

35 |
36 |

The site that opened this window would like to access your GitHub account.

37 | Authorize site to access and update my GitHub account 38 | Don't authorize site 39 |

Learn more at abrah.am/github-anywhere.

40 |
41 | 42 | -------------------------------------------------------------------------------- /appengine/html/index.html: -------------------------------------------------------------------------------- 1 | index -------------------------------------------------------------------------------- /appengine/html/xdr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | xdr 6 | 7 | 8 | 9 | 63 | 64 | 65 | Please go here 66 | 67 | -------------------------------------------------------------------------------- /appengine/images/minibutton_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraham/github-anywhere/7c75e06ce8c9b64742ff712318e2ee35df03c75e/appengine/images/minibutton_icons.png -------------------------------------------------------------------------------- /appengine/images/minibutton_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraham/github-anywhere/7c75e06ce8c9b64742ff712318e2ee35df03c75e/appengine/images/minibutton_matrix.png -------------------------------------------------------------------------------- /appengine/javascript/easyXDM.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * easyXDM 3 | * http://easyxdm.net/ 4 | * Copyright(c) 2009, Øyvind Sean Kinsey, oyvind@kinsey.no. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | (function(I,c,l,F,g,C){var b=this;var j=Math.floor(Math.random()*100)*100;var m=Function.prototype;var L=/^(http.?:\/\/([^\/\s]+))/;var M=/[\-\w]+\/\.\.\//;var A=/([^:])\/\//g;var D="";var k={};var H=I.easyXDM;var P="easyXDM_";var z;function x(S,U){var T=typeof S[U];return T=="function"||(!!(T=="object"&&S[U]))||T=="unknown"}function q(S,T){return !!(typeof(S[T])=="object"&&S[T])}function n(S){return Object.prototype.toString.call(S)==="[object Array]"}var r,t;if(x(I,"addEventListener")){r=function(U,S,T){U.addEventListener(S,T,false)};t=function(U,S,T){U.removeEventListener(S,T,false)}}else{if(x(I,"attachEvent")){r=function(S,U,T){S.attachEvent("on"+U,T)};t=function(S,U,T){S.detachEvent("on"+U,T)}}else{throw new Error("Browser not supported")}}var R=false,E=[],G;if("readyState" in c){G=c.readyState;R=G=="complete"||(~navigator.userAgent.indexOf("AppleWebKit/")&&(G=="loaded"||G=="interactive"))}else{R=!!c.body}function o(){o=m;R=true;for(var S=0;S')}else{T=c.createElement("IFRAME");T.name=S.props.name}T.id=T.name=S.props.name;delete S.props.name;if(S.onLoad){r(T,"load",S.onLoad)}if(typeof S.container=="string"){S.container=c.getElementById(S.container)}if(!S.container){T.style.position="absolute";T.style.left="-2000px";T.style.top="0px";S.container=c.body}T.border=T.frameBorder=0;S.container.insertBefore(T,S.container.firstChild);O(T,S.props);return T}function Q(V,U){if(typeof V=="string"){V=[V]}var T,S=V.length;while(S--){T=V[S];T=new RegExp(T.substr(0,1)=="^"?T:("^"+T.replace(/(\*)/g,".$1").replace(/\?/g,".")+"$"));if(T.test(U)){return true}}return false}function h(U){var Z=U.protocol,T;U.isHost=U.isHost||p(N.xdm_p);if(!U.props){U.props={}}if(!U.isHost){U.channel=N.xdm_c;U.secret=N.xdm_s;U.remote=N.xdm_e;Z=N.xdm_p;if(U.acl&&!Q(U.acl,U.remote)){throw new Error("Access denied for "+U.remote)}}else{U.remote=w(U.remote);U.channel=U.channel||"default"+j++;U.secret=Math.random().toString(16).substring(2);if(p(Z)){if(f(l.href)==f(U.remote)){Z="4"}else{if(x(I,"postMessage")||x(c,"postMessage")){Z="1"}else{if(x(I,"ActiveXObject")&&x(I,"execScript")){Z="3"}else{if(navigator.product==="Gecko"&&"frameElement" in I&&navigator.userAgent.indexOf("WebKit")==-1){Z="5"}else{if(U.remoteHelper){U.remoteHelper=w(U.remoteHelper);Z="2"}else{Z="0"}}}}}}}switch(Z){case"0":O(U,{interval:100,delay:2000,useResize:true,useParent:false,usePolling:false},true);if(U.isHost){if(!U.local){var X=l.protocol+"//"+l.host,S=c.body.getElementsByTagName("img"),Y;var V=S.length;while(V--){Y=S[V];if(Y.src.substring(0,X.length)===X){U.local=Y.src;break}}if(!U.local){U.local=I}}var W={xdm_c:U.channel,xdm_p:0};if(U.local===I){U.usePolling=true;U.useParent=true;U.local=l.protocol+"//"+l.host+l.pathname+l.search;W.xdm_e=U.local;W.xdm_pa=1}else{W.xdm_e=w(U.local)}if(U.container){U.useResize=false;W.xdm_po=1}U.remote=K(U.remote,W)}else{O(U,{channel:N.xdm_c,remote:N.xdm_e,useParent:!p(N.xdm_pa),usePolling:!p(N.xdm_po),useResize:U.useParent?false:U.useResize})}T=[new k.stack.HashTransport(U),new k.stack.ReliableBehavior({}),new k.stack.QueueBehavior({encode:true,maxLength:4000-U.remote.length}),new k.stack.VerifyBehavior({initiate:U.isHost})];break;case"1":T=[new k.stack.PostMessageTransport(U)];break;case"2":T=[new k.stack.NameTransport(U),new k.stack.QueueBehavior(),new k.stack.VerifyBehavior({initiate:U.isHost})];break;case"3":T=[new k.stack.NixTransport(U)];break;case"4":T=[new k.stack.SameOriginTransport(U)];break;case"5":T=[new k.stack.FrameElementTransport(U)];break}T.push(new k.stack.QueueBehavior({lazy:U.lazy,remove:true}));return T}function y(V){var W,U={incoming:function(Y,X){this.up.incoming(Y,X)},outgoing:function(X,Y){this.down.outgoing(X,Y)},callback:function(X){this.up.callback(X)},init:function(){this.down.init()},destroy:function(){this.down.destroy()}};for(var T=0,S=V.length;T<\/script>')}}};(function(){var S={};k.Fn={set:function(T,U){S[T]=U},get:function(U,T){var V=S[U];if(T){delete S[U]}return V}}}());k.Socket=function(T){var S=y(h(T).concat([{incoming:function(W,V){T.onMessage(W,V)},callback:function(V){if(T.onReady){T.onReady(V)}}}])),U=f(T.remote);this.origin=f(T.remote);this.destroy=function(){S.destroy()};this.postMessage=function(V){S.outgoing(V,U)};S.init()};k.Rpc=function(U,T){if(T.local){for(var W in T.local){if(T.local.hasOwnProperty(W)){var V=T.local[W];if(typeof V==="function"){T.local[W]={method:V}}}}}var S=y(h(U).concat([new k.stack.RpcBehavior(this,T),{callback:function(X){if(U.onReady){U.onReady(X)}}}]));this.origin=f(U.remote);this.destroy=function(){S.destroy()};S.init()};k.stack.SameOriginTransport=function(T){var U,W,V,S;return(U={outgoing:function(Y,Z,X){V(Y);if(X){X()}},destroy:function(){if(W){W.parentNode.removeChild(W);W=null}},onDOMReady:function(){S=f(T.remote);if(T.isHost){O(T.props,{src:K(T.remote,{xdm_e:l.protocol+"//"+l.host+l.pathname,xdm_c:T.channel,xdm_p:4}),name:P+T.channel+"_provider"});W=v(T);k.Fn.set(T.channel,function(X){V=X;F(function(){U.up.callback(true)},0);return function(Y){U.up.incoming(Y,S)}})}else{V=i().Fn.get(T.channel,true)(function(X){U.up.incoming(X,S)});F(function(){U.up.callback(true)},0)}},init:function(){B(U.onDOMReady,U)}})};k.stack.PostMessageTransport=function(V){var X,Y,T,U;function S(Z){if(Z.origin){return Z.origin}if(Z.uri){return f(Z.uri)}if(Z.domain){return l.protocol+"//"+Z.domain}throw"Unable to retrieve the origin of the event"}function W(aa){var Z=S(aa);if(Z==U&&aa.data.substring(0,V.channel.length+1)==V.channel+" "){X.up.incoming(aa.data.substring(V.channel.length+1),Z)}}return(X={outgoing:function(aa,ab,Z){T.postMessage(V.channel+" "+aa,ab||U);if(Z){Z()}},destroy:function(){t(I,"message",W);if(Y){T=null;Y.parentNode.removeChild(Y);Y=null}},onDOMReady:function(){U=f(V.remote);if(V.isHost){r(I,"message",function Z(aa){if(aa.data==V.channel+"-ready"){T=("postMessage" in Y.contentWindow)?Y.contentWindow:Y.contentWindow.document;t(I,"message",Z);r(I,"message",W);F(function(){X.up.callback(true)},0)}});O(V.props,{src:K(V.remote,{xdm_e:l.protocol+"//"+l.host,xdm_c:V.channel,xdm_p:1}),name:P+V.channel+"_provider"});Y=v(V)}else{r(I,"message",W);T=("postMessage" in I.parent)?I.parent:I.parent.document;T.postMessage(V.channel+"-ready",U);F(function(){X.up.callback(true)},0)}},init:function(){B(X.onDOMReady,X)}})};k.stack.FrameElementTransport=function(T){var U,W,V,S;return(U={outgoing:function(Y,Z,X){V.call(this,Y);if(X){X()}},destroy:function(){if(W){W.parentNode.removeChild(W);W=null}},onDOMReady:function(){S=f(T.remote);if(T.isHost){O(T.props,{src:K(T.remote,{xdm_e:l.protocol+"//"+l.host+l.pathname+l.search,xdm_c:T.channel,xdm_p:5}),name:P+T.channel+"_provider"});W=v(T);W.fn=function(X){delete W.fn;V=X;F(function(){U.up.callback(true)},0);return function(Y){U.up.incoming(Y,S)}}}else{I.parent.location=N.xdm_e+"#";V=I.frameElement.fn(function(X){U.up.incoming(X,S)});U.up.callback(true)}},init:function(){B(U.onDOMReady,U)}})};k.stack.NixTransport=function(T){var V,X,W,S,U;return(V={outgoing:function(Z,aa,Y){W(Z);if(Y){Y()}},destroy:function(){U=null;if(X){X.parentNode.removeChild(X);X=null}},onDOMReady:function(){S=f(T.remote);if(T.isHost){try{if(!x(I,"getNixProxy")){I.execScript("Class NixProxy\n Private m_parent, m_child, m_Auth\n\n Public Sub SetParent(obj, auth)\n If isEmpty(m_Auth) Then m_Auth = auth\n SET m_parent = obj\n End Sub\n Public Sub SetChild(obj)\n SET m_child = obj\n m_parent.ready()\n End Sub\n\n Public Sub SendToParent(data, auth)\n If m_Auth = auth Then m_parent.send(CStr(data))\n End Sub\n Public Sub SendToChild(data, auth)\n If m_Auth = auth Then m_child.send(CStr(data))\n End Sub\nEnd Class\nFunction getNixProxy()\n Set GetNixProxy = New NixProxy\nEnd Function\n","vbscript")}U=getNixProxy();U.SetParent({send:function(aa){V.up.incoming(aa,S)},ready:function(){F(function(){V.up.callback(true)},0)}},T.secret);W=function(aa){U.SendToChild(aa,T.secret)}}catch(Z){throw new Error("Could not set up VBScript NixProxy:"+Z.message)}O(T.props,{src:K(T.remote,{xdm_e:l.protocol+"//"+l.host+l.pathname+l.search,xdm_c:T.channel,xdm_s:T.secret,xdm_p:3}),name:P+T.channel+"_provider"});X=v(T);X.contentWindow.opener=U}else{I.parent.location=N.xdm_e+"#";try{U=I.opener}catch(Y){throw new Error("Cannot access window.opener")}U.SetChild({send:function(aa){b.setTimeout(function(){V.up.incoming(aa,S)},0)}});W=function(aa){U.SendToParent(aa,T.secret)};F(function(){V.up.callback(true)},0)}},init:function(){B(V.onDOMReady,V)}})};k.stack.NameTransport=function(W){var X;var Z,ad,V,ab,ac,T,S;function aa(ag){var af=W.remoteHelper+(Z?"#_3":"#_2")+W.channel;ad.contentWindow.sendMessage(ag,af)}function Y(){if(Z){if(++ab===2||!Z){X.up.callback(true)}}else{aa("ready");X.up.callback(true)}}function ae(af){X.up.incoming(af,T)}function U(){if(ac){F(function(){ac(true)},0)}}return(X={outgoing:function(ag,ah,af){ac=af;aa(ag)},destroy:function(){ad.parentNode.removeChild(ad);ad=null;if(Z){V.parentNode.removeChild(V);V=null}},onDOMReady:function(){Z=W.isHost;ab=0;T=f(W.remote);W.local=w(W.local);if(Z){k.Fn.set(W.channel,function(ag){if(Z&&ag==="ready"){k.Fn.set(W.channel,ae);Y()}});S=K(W.remote,{xdm_e:W.local,xdm_c:W.channel,xdm_p:2});O(W.props,{src:S+"#"+W.channel,name:P+W.channel+"_provider"});V=v(W)}else{W.remoteHelper=W.remote;k.Fn.set(W.channel,ae)}ad=v({props:{src:W.local+"#_4"+W.channel},onLoad:function af(){t(ad,"load",af);k.Fn.set(W.channel+"_load",U);(function ag(){if(typeof ad.contentWindow.sendMessage=="function"){Y()}else{F(ag,50)}}())}})},init:function(){B(X.onDOMReady,X)}})};k.stack.HashTransport=function(U){var X;var ac=this,aa,V,S,Y,ah,W,ag;var ab,T;function af(aj){if(!ag){return}var ai=U.remote+"#"+(ah++)+"_"+aj;((aa||!ab)?ag.contentWindow:ag).location=ai}function Z(ai){Y=ai;X.up.incoming(Y.substring(Y.indexOf("_")+1),T)}function ae(){if(!W){return}var ai=W.location.href,ak="",aj=ai.indexOf("#");if(aj!=-1){ak=ai.substring(aj)}if(ak&&ak!=Y){Z(ak)}}function ad(){V=setInterval(ae,S)}return(X={outgoing:function(ai,aj){af(ai)},destroy:function(){I.clearInterval(V);if(aa||!ab){ag.parentNode.removeChild(ag)}ag=null},onDOMReady:function(){aa=U.isHost;S=U.interval;Y="#"+U.channel;ah=0;ab=U.useParent;T=f(U.remote);if(aa){U.props={src:U.remote,name:P+U.channel+"_provider"};if(ab){U.onLoad=function(){W=I;ad();X.up.callback(true)}}else{var ak=0,ai=U.delay/50;(function aj(){if(++ak>ai){throw new Error("Unable to reference listenerwindow")}try{W=ag.contentWindow.frames[P+U.channel+"_consumer"]}catch(al){}if(W){ad();X.up.callback(true)}else{F(aj,50)}}())}ag=v(U)}else{W=I;ad();if(ab){ag=parent;X.up.callback(true)}else{O(U,{props:{src:U.remote+"#"+U.channel+new Date(),name:P+U.channel+"_consumer"},onLoad:function(){X.up.callback(true)}});ag=v(U)}}},init:function(){B(X.onDOMReady,X)}})};k.stack.ReliableBehavior=function(T){var V,X;var W=0,S=0,U="";return(V={incoming:function(aa,Y){var Z=aa.indexOf("_"),ab=aa.substring(0,Z).split(",");aa=aa.substring(Z+1);if(ab[0]==W){U="";if(X){X(true)}}if(aa.length>0){V.down.outgoing(ab[1]+","+W+"_"+U,Y);if(S!=ab[1]){S=ab[1];V.up.incoming(aa,Y)}}},outgoing:function(aa,Y,Z){U=aa;X=Z;V.down.outgoing(S+","+(++W)+"_"+aa,Y)}})};k.stack.QueueBehavior=function(U){var X,Y=[],ab=true,V="",aa,S=0,T=false,W=false;function Z(){if(U.remove&&Y.length===0){s(X);return}if(ab||Y.length===0||aa){return}ab=true;var ac=Y.shift();X.down.outgoing(ac.data,ac.origin,function(ad){ab=false;if(ac.callback){F(function(){ac.callback(ad)},0)}Z()})}return(X={init:function(){if(p(U)){U={}}if(U.maxLength){S=U.maxLength;W=true}if(U.lazy){T=true}else{X.down.init()}},callback:function(ad){ab=false;var ac=X.up;Z();ac.callback(ad)},incoming:function(af,ad){if(W){var ae=af.indexOf("_"),ac=parseInt(af.substring(0,ae),10);V+=af.substring(ae+1);if(ac===0){if(U.encode){V=g(V)}X.up.incoming(V,ad);V=""}}else{X.up.incoming(af,ad)}},outgoing:function(ag,ad,af){if(U.encode){ag=C(ag)}var ac=[],ae;if(W){while(ag.length!==0){ae=ag.substring(0,S);ag=ag.substring(ae.length);ac.push(ae)}while((ae=ac.shift())){Y.push({data:ac.length+"_"+ae,origin:ad,callback:ac.length===0?af:null})}}else{Y.push({data:ag,origin:ad,callback:af})}if(T){X.down.init()}else{Z()}},destroy:function(){aa=true;X.down.destroy()}})};k.stack.VerifyBehavior=function(W){var X,V,T,U=false;function S(){V=Math.random().toString(16).substring(2);X.down.outgoing(V)}return(X={incoming:function(aa,Y){var Z=aa.indexOf("_");if(Z===-1){if(aa===V){X.up.callback(true)}else{if(!T){T=aa;if(!W.initiate){S()}X.down.outgoing(aa)}}}else{if(aa.substring(0,Z)===T){X.up.incoming(aa.substring(Z+1),Y)}}},outgoing:function(aa,Y,Z){X.down.outgoing(V+"_"+aa,Y,Z)},callback:function(Y){if(W.initiate){S()}}})};k.stack.RpcBehavior=function(Y,T){var V,aa=T.serializer||J();var Z=0,X={};function S(ab){ab.jsonrpc="2.0";V.down.outgoing(aa.stringify(ab))}function W(ab,ad){var ac=Array.prototype.slice;return function(){var ae=arguments.length,ag,af={method:ad};if(ae>0&&typeof arguments[ae-1]==="function"){if(ae>1&&typeof arguments[ae-2]==="function"){ag={success:arguments[ae-2],error:arguments[ae-1]};af.params=ac.call(arguments,0,ae-2)}else{ag={success:arguments[ae-1]};af.params=ac.call(arguments,0,ae-1)}X[""+(++Z)]=ag;af.id=Z}else{af.params=ac.call(arguments,0)}if(ab.namedParams&&af.params.length===1){af.params=af.params[0]}S(af)}}function U(ai,ah,ad,ag){if(!ad){if(ah){S({id:ah,error:{code:-32601,message:"Procedure not found."}})}return}var af,ac;if(ah){af=function(aj){af=m;S({id:ah,result:aj})};ac=function(aj,ak){ac=m;var al={id:ah,error:{code:-32099,message:aj}};if(ak){al.error.data=ak}S(al)}}else{af=ac=m}if(!n(ag)){ag=[ag]}try{var ab=ad.method.apply(ad.scope,ag.concat([af,ac]));if(!p(ab)){af(ab)}}catch(ae){ac(ae.message)}}return(V={incoming:function(ac,ab){var ad=aa.parse(ac);if(ad.method){if(T.handle){T.handle(ad,S)}else{U(ad.method,ad.id,T.local[ad.method],ad.params)}}else{var ae=X[ad.id];if(ad.error){if(ae.error){ae.error(ad.error)}}else{if(ae.success){ae.success(ad.result)}}delete X[ad.id]}},init:function(){if(T.remote){for(var ab in T.remote){if(T.remote.hasOwnProperty(ab)){Y[ab]=W(T.remote[ab],ab)}}}V.down.init()},destroy:function(){for(var ab in T.remote){if(T.remote.hasOwnProperty(ab)&&Y.hasOwnProperty(ab)){delete Y[ab]}}V.down.destroy()}})};b.easyXDM=k})(window,document,location,window.setTimeout,decodeURIComponent,encodeURIComponent); -------------------------------------------------------------------------------- /appengine/javascript/github-anywhere.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var G = {}, 3 | rpc = false, 4 | loaded = { 5 | jQuery: typeof window.jQuery === 'function' || false, 6 | easyXDM: typeof window.easyXDM === 'object' || false, 7 | style: false 8 | }, 9 | readyQueue = [], 10 | config = { 11 | xdrURL: 'https://githubanywhere.appspot.com/xdr.html', 12 | clientID: '605fdb0289347957e1b1', 13 | redirectURI: 'https://githubanywhere.appspot.com/callback.html', 14 | scope: 'user,public_repo,gist', 15 | easyXDMSource: 'https://githubanywhere.appspot.com/javascript/easyXDM.min.js', 16 | jQuerySource: 'https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js', 17 | styleSource: 'https://githubanywhere.appspot.com/style/style.css' 18 | }; 19 | 20 | // Fix issues with browsers that don't have consoles enabled. 21 | if (!('console' in window)) { 22 | window.console = { 23 | log: function () {}, 24 | info: function () {}, 25 | error: function () {} 26 | }; 27 | } 28 | 29 | function GHA(callback) { 30 | console.log('GHA()'); 31 | ready(callback); 32 | } 33 | 34 | function ready(callback) { 35 | console.log('ready()'); 36 | if (externalIsLoaded()) { 37 | $(document).ready(function () { 38 | callback(G); 39 | }); 40 | } else { 41 | readyQueue.push(callback); 42 | } 43 | }; 44 | 45 | if (typeof window.GitHubAnywhere === 'function') { 46 | // GitHubAnywhere already loaded for whatever reason. 47 | return; 48 | } else { 49 | console.log('Adding GitHubAnywhere to window'); 50 | window.GitHubAnywhere = GHA; 51 | 52 | load(); 53 | } 54 | 55 | function load() { 56 | console.log('load()'); 57 | var head = document.getElementsByTagName('head')[0]; 58 | 59 | if (!loaded.jQuery) { 60 | appendScript({ 61 | source: 'jQuerySource', 62 | name: 'jQuery', 63 | head: head 64 | }); 65 | } 66 | 67 | if (!loaded.easyXDM) { 68 | appendScript({ 69 | source: 'easyXDMSource', 70 | name: 'easyXDM', 71 | head: head 72 | }); 73 | } 74 | 75 | if (!loaded.style) { 76 | console.log('Stylesheet finished loading'); 77 | var s = document.createElement('link'); 78 | s.rel = 'stylesheet'; 79 | s.href = config.styleSource; 80 | loaded.style = true; 81 | 82 | head.appendChild(s); 83 | } 84 | 85 | finishedLoad(); 86 | } 87 | 88 | /** 89 | * Appends a script elemnt to . 90 | * @param {object} options containing parameters of: 91 | * @param {string} source url from Config 92 | * @param {string} name of script being loaded 93 | * @param {object} head element to append script to 94 | * @private 95 | */ 96 | function appendScript(options) { 97 | console.log('appendScript()'); 98 | var script = document.createElement('script'); 99 | script.src = config[options.source];// easyXDMSource; 100 | script.onload = function () { 101 | loaded[options.name] = true; 102 | console.log(options.name + ' finished loading'); 103 | finishedLoad(); 104 | }; 105 | options.head.appendChild(script); 106 | } 107 | 108 | /** 109 | * Checks to see if all scripts and stylesheets have loaded. 110 | * @returns {boolean} 111 | * @private 112 | */ 113 | function externalIsLoaded() { 114 | console.log('externalIsLoaded()'); 115 | return loaded.jQuery && loaded.easyXDM && loaded.style; 116 | } 117 | 118 | /** 119 | * If externals have loaded fire init() 120 | * @private 121 | */ 122 | function finishedLoad() { 123 | console.log('finishedLoad()'); 124 | if (externalIsLoaded()) { 125 | init(); 126 | } 127 | } 128 | 129 | /** 130 | * Create an easyXDM provider if none exist and fire any queued functions. 131 | * @private 132 | */ 133 | function init() { 134 | console.log('init()'); 135 | if (!rpc) { 136 | rpc = new easyXDM.Rpc({ 137 | remote: config.xdrURL 138 | }, 139 | { 140 | local: { 141 | get: function (successCallback, errorCallback) {}, 142 | post: function (successCallback, errorCallback) {}, 143 | startAuthentication: function (successCallback, errorCallback) {}, 144 | completeAuthentication: function (options, successCallback, errorCallback) {} 145 | }, 146 | remote: { 147 | get: {}, 148 | post: {}, 149 | startAuthentication: {}, 150 | completeAuthentication: {} 151 | } 152 | }); 153 | } 154 | 155 | if (readyQueue.length > 0) { 156 | $.each(readyQueue, function (index, Fn) { 157 | Fn(G); 158 | }); 159 | } 160 | } 161 | 162 | /** 163 | * Starts a scan of the page for GHA buttons. 164 | * @param {string} selector for jQuery to find 165 | */ 166 | G.buttons = function (selector) { 167 | console.log('G.buttons()'); 168 | scanPage(selector); 169 | } 170 | 171 | /** 172 | * Scans the page for GHA classes to attach appropriate actions. 173 | * @param {string} selector for jQuery to find 174 | * @default {string} 'a.github-anywhere' 175 | * @private 176 | */ 177 | function scanPage(selector) { 178 | console.log('scanPage()'); 179 | var $github, $link, user, repo, 180 | selector = selector ? selector : 'a.github-anywhere', 181 | following = JSON.parse(get('following')); 182 | 183 | $github = $(selector); 184 | $github.each(function () { 185 | var $this = $(this); 186 | if ($this.hasClass('github-anywhere-watch')) { 187 | $this.html("" + $this.html() + ''); 188 | if (get('watching_' + $this.attr('data-user') + '/' + $this.attr('data-repo'))) { 189 | $this.addClass('github-anywhere-enabled'); 190 | $this.find('span').html('Unwatch ' + $this.attr('data-user') + '/' + $this.attr('data-repo') + ' on GitHub'); 191 | } else { 192 | $this.removeClass('github-anywhere-enabled'); 193 | $this.find('span').html('Watch ' + $this.attr('data-user') + '/' + $this.attr('data-repo') + ' on GitHub'); 194 | } 195 | } else { 196 | $this.html('' + $this.html() + ''); 197 | if ($.inArray($this.attr('data-user'), following) !== -1) { 198 | $this.addClass('github-anywhere-enabled'); 199 | $this.children().text('Unfollow ' + $this.attr('data-user') + ' on GitHub'); 200 | } 201 | } 202 | }); 203 | $github.addClass('github-anywhere-minibutton'); 204 | $github.click(function () { 205 | console.log('click()'); 206 | $link = $(this); 207 | user = $link.attr('data-user'); 208 | repo = $link.attr('data-repo'); 209 | if (user && !repo) { 210 | if (get('accessToken')) { 211 | toggleFollowing({ selection: $link }); 212 | } else { 213 | set('nextAction', JSON.stringify({ action: 'toggle', type: 'user', target: user })); 214 | startAuthentication(); 215 | } 216 | } else if (user && repo) { 217 | if (get('accessToken')) { 218 | toggleWatching({ selection: $link }); 219 | } else { 220 | set('nextAction', JSON.stringify({ 221 | action: 'toggle', 222 | type: 'repo', 223 | target: { 224 | repo: repo, 225 | user: user 226 | } 227 | })); 228 | startAuthentication(); 229 | } 230 | } 231 | return false; 232 | }) 233 | 234 | $('a.github-anywhere-minibutton').bind({ 235 | mousedown: function () { 236 | $(this).addClass('github-anywhere-mousedown'); 237 | }, 238 | blur: function () { 239 | $(this).removeClass('github-anywhere-mousedown'); 240 | }, 241 | mouseup: function () { 242 | $(this).removeClass('github-anywhere-mousedown'); 243 | } 244 | }); 245 | } 246 | 247 | /** 248 | * Opens a popup to github.com to authenticate user and starts checkForCode loop. 249 | * @returns {boolean} 250 | * @default {boolean} false 251 | * @private 252 | */ 253 | function startAuthentication() { 254 | console.log('startAuthentication()'); 255 | var child, 256 | url = 'https://github.com/login/oauth/authorize'; 257 | url += '?client_id=' + config.clientID + '&redirect_uri=' + config.redirectURI + '&scope=' + config.scope; 258 | child = window.open(url, '', 'width=975,height=600'); 259 | checkForCode(); 260 | return false; 261 | } 262 | 263 | /** 264 | * User has finished authentication flow so perform action delayed for authentication. 265 | * @private 266 | */ 267 | function completeAuthentication(options) { 268 | console.log('completeAuthentication()'); 269 | set('accessToken', options.accessToken); 270 | performNextAction(); 271 | }; 272 | 273 | /** 274 | * Checks to see if user has copmleted auth flow every 500 ms. 275 | * @param {object} options passed from github if auth failed 276 | * @private 277 | */ 278 | function checkForCode(options) { 279 | console.log('checkForCode()'); 280 | if (options && options.message && options.message.error) { 281 | // GitHub returned an error 282 | 283 | } else { 284 | rpc.completeAuthentication(completeAuthentication, function () { 285 | setTimeout(checkForCode, 500, options); 286 | }); 287 | } 288 | } 289 | 290 | /** 291 | * Authentication flow has finished so perform action started when unauthenticated. 292 | * @private 293 | */ 294 | function performNextAction() { 295 | console.log('performNextAction()'); 296 | var nextAction = get('nextAction'); 297 | if (nextAction) { 298 | nextAction = JSON.parse(nextAction); 299 | remove('nextAction'); 300 | switch (nextAction.type) { 301 | case 'user': 302 | $('a.github-anywhere[data-user="' + nextAction.target + '"]:first').click(); 303 | break; 304 | case 'repo': 305 | $('a.github-anywhere[data-repo="' + nextAction.target.repo + '"][data-user="' + nextAction.target.user + '"]:first').click(); 306 | break; 307 | } 308 | } 309 | } 310 | 311 | function isFollowing(user) { 312 | var following = localStorage['github_anywhere_cache_["user/show/' + user + '/following"]']; 313 | if (!following) { 314 | rpc.following(user, function (response){ 315 | 316 | }, function (errorObj){ 317 | 318 | }); 319 | } 320 | } 321 | 322 | function updateFollowing(user) { 323 | rpc.following(user, function (response){ 324 | localStorage['github_anywhere_cache_["user/show/' + user + '/following"]'] = JSON.stringify(response); 325 | }, function (errorObj){ 326 | 327 | }); 328 | } 329 | 330 | function getFollowing(user) { 331 | return JSON.parse(localStorage['github_anywhere_cache_["user/show/' + user + '/following"]']); 332 | } 333 | 334 | function toggleFollowing(options) { 335 | console.log('toggleFollowing()'); 336 | var $link = $(options.selection), user = $link.attr('data-user'); 337 | if ($link.hasClass('github-anywhere-enabled')) { 338 | rpc.post({ path: '/user/unfollow/' + user, parameters: { access_token: get('accessToken') } }, function (response){ 339 | console.log('rpc.post(user/unfollow)'); 340 | $link.removeClass('github-anywhere-enabled'); 341 | $link.children().text('Follow ' + user + ' on GitHub'); 342 | set('following', JSON.stringify(response.users)); 343 | }, function (errorObj){ 344 | 345 | }); 346 | } else { 347 | rpc.post({ path: '/user/follow/' + user, parameters: { access_token: get('accessToken') } }, function (response){ 348 | console.log('rpc.post(user/follow)'); 349 | $link.addClass('github-anywhere-enabled'); 350 | $link.children().text('Unfollow ' + user + ' on GitHub'); 351 | set('following', JSON.stringify(response.users)); 352 | }, function (errorObj){ 353 | 354 | }); 355 | } 356 | } 357 | 358 | function toggleWatching(options) { 359 | console.log('toggleWatching()'); 360 | var $link = $(options.selection), user = $link.attr('data-user'), repo = $link.attr('data-repo'); 361 | if ($link.hasClass('github-anywhere-enabled')) { 362 | rpc.post({ path: '/repos/unwatch/' + user + '/' + repo, parameters: { access_token: get('accessToken') } }, function (response){ 363 | console.log('rpc.post(repos/unwatch)'); 364 | $link.removeClass('github-anywhere-enabled'); 365 | $link.find('span').html('Watch ' + user + '/' + repo + ' on GitHub'); 366 | remove('watching_' + response.repository.owner + '/' + response.repository.name); 367 | }, function (errorObj){ 368 | 369 | }); 370 | } else { 371 | rpc.post({ path: '/repos/watch/' + user + '/' + repo, parameters: { access_token: get('accessToken') } }, function (response){ 372 | console.log('rpc.post(repos/watch)'); 373 | $link.addClass('github-anywhere-enabled'); 374 | $link.find('span').html('Unwatch ' + user + '/' + repo + ' on GitHub'); 375 | set('watching_' + response.repository.owner + '/' + response.repository.name, true); 376 | }, function (errorObj){ 377 | 378 | }); 379 | } 380 | } 381 | 382 | /** 383 | * Get value from localStorage. 384 | * @param {string} key of value to get 385 | * @returns {mixed} value 386 | * @default {boolean} false 387 | * @private 388 | */ 389 | function get(key) { 390 | console.log('get()'); 391 | return localStorage.getItem('gitHubAnywhere_' + key) && localStorage.getItem('gitHubAnywhere_' + key) != 'undefined' ? localStorage.getItem('gitHubAnywhere_' + key) : false; 392 | } 393 | 394 | /** 395 | * Set value to localStorage. 396 | * @param {string} key of value to set 397 | * @param {mixed} value to set 398 | * @private 399 | */ 400 | function set(key, value) { 401 | console.log('set()'); 402 | localStorage.setItem('gitHubAnywhere_' + key, value); 403 | localStorage.setItem('gitHubAnywhere_' + key + 'Time', getTime()); 404 | } 405 | 406 | /** 407 | * Remove key and value from localStogae. 408 | * @param {string} key of value to remove 409 | * @private 410 | */ 411 | function remove(key) { 412 | console.log('remove()'); 413 | localStorage.removeItem('gitHubAnywhere_' + key); 414 | localStorage.removeItem('gitHubAnywhere_' + key + 'Time'); 415 | } 416 | 417 | /** 418 | * Get the current time in milliseconds. 419 | * @returns {integer} 420 | * @private 421 | */ 422 | function getTime() { 423 | console.log('getTime()'); 424 | var d = new Date(); 425 | return d.getTime(); 426 | } 427 | })(); -------------------------------------------------------------------------------- /appengine/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | from google.appengine.ext import webapp 19 | from google.appengine.ext.webapp import util 20 | from google.appengine.api import urlfetch 21 | from django.utils import simplejson as json 22 | 23 | base = 'https://github.com/login/oauth/access_token' 24 | client_id = '?client_id=' 25 | redirect_url = '&redirect_uri=https://githubanywhere.appspot.com/callback.html' 26 | client_secret = '&client_secret=' 27 | 28 | class MainHandler(webapp.RequestHandler): 29 | def get(self): 30 | self.response.headers['Content-Type'] = 'text/plain' 31 | self.response.out.write('Hello world!') 32 | 33 | class APIv0LoginOauthAccessTokenHandler(webapp.RequestHandler): 34 | def post(self): 35 | self.response.headers['Content-Type'] = 'text/plain' 36 | # self.response.out.write('APIv0UserFollowHandler\n') 37 | user = GetUserFromPath(self.request.path) 38 | code = self.request.get('code') 39 | url = base + client_id + redirect_url + client_secret + '&code=' + code 40 | result = urlfetch.fetch(url, None, 'POST') 41 | self.response.out.write(result.content) 42 | 43 | class APIv0Handler(webapp.RequestHandler): 44 | def get(self): 45 | self.response.headers['Content-Type'] = 'application/json' 46 | # self.response.out.write('APIv0Handler\n') 47 | url = 'https://github.com/api/v2/json' + self.request.path 48 | access_token = self.request.get('access_token') 49 | if access_token: 50 | url += '?access_token=' + access_token 51 | result = urlfetch.fetch(url, None, 'GET') 52 | self.response.out.write(result.content) 53 | def post(self): 54 | self.response.headers['Content-Type'] = 'application/json' 55 | # self.response.out.write('APIv0UserFollowHandler\n') 56 | url = 'https://github.com/api/v2/json' + self.request.path 57 | access_token = self.request.get('access_token') 58 | if access_token: 59 | url += '?access_token=' + access_token 60 | result = urlfetch.fetch(url, None, 'POST') 61 | self.response.out.write(result.content) 62 | 63 | 64 | def GetUserFromPath(path, element = 3): 65 | return path.split('/')[element] 66 | 67 | def main(): 68 | application = webapp.WSGIApplication([ 69 | ('/', MainHandler), 70 | ('/login/oauth/access_token', APIv0LoginOauthAccessTokenHandler), 71 | ('/.*', APIv0Handler), 72 | ], 73 | debug=True) 74 | util.run_wsgi_app(application) 75 | 76 | 77 | if __name__ == '__main__': 78 | main() -------------------------------------------------------------------------------- /appengine/style/style.css: -------------------------------------------------------------------------------- 1 | /* button basics */ 2 | a.github-anywhere-minibutton { 3 | display:inline-block; 4 | height:23px; 5 | padding:0 0 0 3px; 6 | font-size:11px; 7 | font-weight:bold; 8 | color:#333; 9 | text-shadow:1px 1px 0 #fff; 10 | background:url(https://githubanywhere.appspot.com/images/minibutton_matrix.png) 0 0 no-repeat; 11 | white-space:nowrap; 12 | border:none; 13 | overflow:visible; 14 | cursor:pointer; 15 | text-decoration:none; 16 | } 17 | 18 | a.github-anywhere-minibutton > span { 19 | display:block; 20 | height:23px; 21 | padding:0 10px 0 8px; 22 | line-height:23px; 23 | background:url(https://githubanywhere.appspot.com/images/minibutton_matrix.png) 100% 0 no-repeat; 24 | } 25 | 26 | a.github-anywhere-minibutton:hover, a.github-anywhere-minibutton:focus { 27 | color:#fff; 28 | text-decoration:none; 29 | text-shadow:-1px -1px 0 rgba(0,0,0,0.3); 30 | background-position:0 -30px; 31 | } 32 | a.github-anywhere-enabled:hover, a.github-anywhere-enabled:focus { 33 | color:#fff; 34 | text-decoration:none; 35 | text-shadow:-1px -1px 0 rgba(0,0,0,0.3); 36 | background-position:0 -90px; 37 | } 38 | a.github-anywhere-minibutton:hover > span, a.github-anywhere-minibutton:focus > span {background-position:100% -30px;} 39 | a.github-anywhere-enabled:hover > span, a.github-anywhere-enabled:focus > span {background-position:100% -90px;} 40 | 41 | a.github-anywhere-minibutton.mousedown{background-position:0 -60px; } 42 | a.github-anywhere-minibutton.mousedown > span{background-position:100% -60px; } 43 | 44 | a.github-anywhere-enabled > span { 45 | display:block; 46 | height:23px; 47 | padding:0 10px 0 8px; 48 | line-height:23px; 49 | background:url(https://githubanywhere.appspot.com/images/minibutton_matrix.png) 100% -90 no-repeat; 50 | } 51 | 52 | /* with icon */ 53 | a.github-anywhere-watch .github-anywhere-icon { 54 | float:left; 55 | margin-left:-4px; 56 | width:18px; 57 | height:22px; 58 | background:url(https://githubanywhere.appspot.com/images/minibutton_icons.png) 0 0 no-repeat; 59 | } 60 | a.github-anywhere-watch .github-anywhere-icon {background-position:-20px 0;} 61 | a.github-anywhere-watch:hover .github-anywhere-icon, a.github-anywhere-watch:focus .github-anywhere-icon {background-position:-20px -25px;} 62 | --------------------------------------------------------------------------------