TouchDevelop/rt/cloud.ts

662 строки
26 KiB
TypeScript
Исходник Обычный вид История

2015-02-05 00:41:44 +03:00
///<reference path='refs.ts'/>
module TDev {
export module Cloud {
export var lite = false;
export function getServiceUrl() { return <string>((<any>window).rootUrl); }
export function mkLegalDiv() {
var link = (text: string, lnk: string) =>
HTML.mkA(null, getServiceUrl() + lnk, "_blank", text);
return div("wall-dialog-body", div("smallText",
lf("Publishing is subject to our "),
link(lf("terms of use"), "/legal"),
lf(". Please read our information about "), link(lf("privacy and cookies"), "/privacy"), "."))
}
export var authenticateAsync = (activity:string, redirect = false, dontRedirect = false): Promise =>
{ // boolean
if (!Cloud.isAccessTokenExpired()) return Promise.as(true);
function loginAsync() {
var loginUrl = Cloud.getServiceUrl() + "/oauth/dialog?response_type=token&"
+ "client_id=webapp"
+ "&identity_provider=" + encodeURIComponent(Cloud.getIdentityProvider() || "");
return TDev.RT.Web.oauth_v2_async(loginUrl, "touchdevelop")
.then((or: TDev.RT.OAuthResponse) => {
if (or.is_error()) return false;
else {
var id = or.others().at('id');
var oldid = Cloud.getUserId();
if (oldid && id != oldid) {
// TODO: error message.
return false;
}
Cloud.setUserId(or.others().at('id'));
Cloud.setAccessToken(encodeURIComponent(or.access_token()));
Cloud.setIdentityProvider(or.others().at('identity_provider'));
return true;
}
});
}
2015-02-05 00:44:02 +03:00
return Cloud.isOnlineWithPingAsync()
.then((isOnline : boolean) => {
if (!isOnline) return Promise.as(false);
2015-02-05 00:41:44 +03:00
var prevHash = (window.location.hash || "#").replace(/#/, "");
var login = (<any>TDev).Login;
if (login) {
if (!login.show || dontRedirect)
login = null;
if (!redirect && (!prevHash || /^(hub|list:.*:user:me:)/.test(prevHash)))
login = null;
}
var r = new PromiseInv();
var m = new ModalDialog();
m.addHTML(
Util.fmt("<h3>{0:q} requires sign&nbsp;in</h3>", activity) +
(!(<any>TDev).TheEditor ? "" :
"<p class='agree'>" +
"After you sign in we will back up and sync scripts between your devices. " +
"You will be able to publish scripts, join and create groups, post comments, post leaderboard scores, and give hearts. " +
"In short, it's totally awesome!" +
"</p>") +
"<p class='agree'>You can sign in with your Microsoft, Google, Facebook or Yahoo account.</p>"
)
m.fullWhite();
var ignoreDismiss = false;
m.add(div("wall-dialog-buttons",
HTML.mkButton(lf("maybe later"), () => { m.dismiss() }, "gray-button"),
HTML.mkButtonElt("wall-button login-button", SVG.getLoginButton()).withClick(() => {
ignoreDismiss = true;
m.dismiss()
if (login) login.show();
else loginAsync().done(v => r.success(v))
})));
m.onDismiss = () => {
if (!ignoreDismiss) r.success(false);
};
m.show();
return r;
})
}
export function anonMode(activity:string, restart:()=>void = null, redirect = false)
{
if (Cloud.isOffline()) {
Cloud.showModalOnlineInfo(lf("{0} requires online access", activity))
return true;
}
if (Cloud.getUserId()) return false;
Cloud.authenticateAsync(activity, redirect).done((ok) => {
if (ok && restart) restart();
})
return true;
}
export function parseAccessToken(h: string, onStateError : () => void, onUserError: () => void ): boolean {
var stateMatch = h.match(/.*&state=([^&]*)/);
var state = stateMatch ? stateMatch[1] : "";
if (Cloud.oauthStates().indexOf(decodeURIComponent(state)) == -1) {
onStateError();
return false;
}
var token = h.match(/.*#access_token=([^&]*)/)[1];
var m = h.match(/.*&identity_provider=([^&]*)/);
var identityProvider = m ? decodeURIComponent(m[1]) : undefined;
var id = h.match(/.*&id=([^&]*)/)[1];
var expires = parseInt((h.match(/.*&expires_in=([^&]*)/)||["0","0"])[1]);
var oldid = Cloud.getUserId();
if (oldid && id != oldid) {
onUserError();
return false;
}
if (/.*[#&]dbg=true/.test(h))
window.localStorage.setItem("dbg", "true")
else
window.localStorage.removeItem("dbg");
Cloud.setUserId(id);
Cloud.setIdentityProvider(identityProvider || "");
Cloud.setAccessToken(token);
return true;
}
export function getAccessToken() : string {
return window.localStorage.getItem("access_token");
}
export function isAccessTokenExpired() : boolean {
return !getAccessToken() || !!window.localStorage.getItem("access_token_expired");
}
export function accessTokenExpired() : void {
window.localStorage.setItem("access_token_expired", "1")
}
export function setAccessToken(token : string) : void {
window.localStorage.removeItem("access_token_expired");
if (!token) window.localStorage.removeItem("access_token");
else window.localStorage.setItem("access_token", token)
}
export var getUserId = () => window.localStorage.getItem("userid");
export var currentReleaseId = "";
export function getWorldId(): string {
var worldId = window.localStorage.getItem("worldId");
if (!worldId) window.localStorage.setItem("worldId", worldId = "$webclient$-" + Util.guidGen())
return worldId;
}
export function oauthStates() {
var a = JSON.parse(window.localStorage.getItem("oauth_states") || "[]");
if (a.length == 0) a = [Random.normalized().toString()];
window.localStorage.setItem("oauth_states", JSON.stringify(a))
return a;
}
export function setUserId(id : string) {
if (!id)
window.localStorage.removeItem("userid");
else
window.localStorage.setItem("userid", id)
}
export function getIdentityProvider() {
return window.localStorage.getItem("identity_provider");
}
export function setIdentityProvider(id : string) {
if (!id)
window.localStorage.removeItem("identity_provider");
else
window.localStorage.setItem("identity_provider", id)
}
export interface Progress {
guid?: string;
index?: number;
completed?: number;
numSteps?: number;
lastUsed?: number;
}
export interface Progresses {
[id: string]: Progress;
}
function mergeProgress(oldData: Progresses, data: Progresses) {
oldData = JSON.parse(JSON.stringify(oldData))
Object.keys(data).forEach(id => {
var oldProgress = oldData[id] || <Progress>{};
var progress = data[id];
if (oldProgress.index === undefined || oldProgress.index <= progress.index) {
if (progress.guid) oldProgress.guid = progress.guid;
oldProgress.index = progress.index
if (progress.completed && (oldProgress.completed === undefined || oldProgress.completed > progress.completed)) oldProgress.completed = progress.completed;
oldProgress.numSteps = progress.numSteps;
oldProgress.lastUsed = progress.lastUsed;
}
oldData[id] = oldProgress;
});
return oldData
}
export function storeProgress(data: Progresses) {
var newData = mergeProgress(loadPendingProgress(), data);
window.localStorage.setItem("progress", JSON.stringify(newData))
window.localStorage.setItem("total_progress", JSON.stringify(mergeProgress(loadProgress(), data)));
}
function clearPendingProgress(data: Progresses) {
var oldData = loadPendingProgress();
Object.keys(data).forEach(id => {
var oldProgress = oldData[id];
var progress = data[id];
if (oldProgress &&
(!oldProgress.guid || !progress.guid) &&
(oldProgress.index === undefined || progress.index === undefined || oldProgress.index <= progress.index) &&
(oldProgress.completed === undefined || progress.completed === undefined || oldProgress.completed >= progress.completed ))
delete oldData[id];
});
window.localStorage.setItem("progress", JSON.stringify(oldData))
}
export function loadProgress() {
return loadPendingProgress("total_progress")
}
function loadPendingProgress(name = "progress") {
return <Progresses>JSON.parse(window.localStorage.getItem(name) || "{}");
}
2015-02-05 00:44:02 +03:00
export function isOffline() : boolean {
return !isOnline();
}
2015-02-05 00:41:44 +03:00
export function isOnline() : boolean {
var b = !TDev.Browser.noNetwork && (TDev.Browser.isNodeJS || window.navigator.onLine) && isTouchDevelopOnline();
// randomly turns off connectivity
if (TDev.dbg && b && isChaosOffline() && TDev.RT.Math_.random(10) < 4)
b = false;
return b;
}
2015-02-05 00:44:02 +03:00
export function isOnlineWithPingAsync() : Promise { // of boolean
if (!isOnline()) return Promise.as(false);
return pingAsync();
}
2015-02-05 00:41:44 +03:00
export var transientOfflineMode = false;
export function isTouchDevelopOnline() : boolean {
return !window.localStorage.getItem('offline_mode') && !transientOfflineMode;
}
export function setTouchDevelopOnline(value: boolean) {
if (value)
window.localStorage.removeItem('offline_mode');
else
window.localStorage.setItem('offline_mode', "true")
}
export function isChaosOffline() : boolean {
return !!window.localStorage.getItem('chaos_offline_mode');
}
export function setChaosOffline(value: boolean) {
if (!value)
window.localStorage.removeItem('chaos_offline_mode');
else
window.localStorage.setItem('chaos_offline_mode', "true")
}
export function offlineErrorAsync(): Promise {
var msg = isTouchDevelopOnline() ? "offline mode is on" : "force offline mode is on";
return new Promise((onSuccess, onError, onProgress) => {
var e = new Error(msg);
(<any>e).status = 502;
onError(e);
});
}
export function canPublish()
{
return getUserId() != "paema";
}
export function onlineInfo(): string {
if (Cloud.isOffline()) {
var msg = lf("You appear to be offline. ") + (isTouchDevelopOnline()
? lf("Please connect to the internet.")
: lf("Please go to the settings in the main hub to disable offline mode."));
return msg;
}
else {
return lf("You are online.");
}
}
export function showOnlineInfoProgess() {
HTML.showProgressNotification(onlineInfo(), true);
}
export function showModalOnlineInfo(title : string) {
ModalDialog.info(title, onlineInfo());
}
var appendAccessToken = (url: string) => (url + (/\?/.test(url) ? "&" : "?") + "access_token=" + getAccessToken() + "&world_id=" + encodeURIComponent(Cloud.getWorldId()) + "&release_id=" + encodeURIComponent(Cloud.currentReleaseId) + "&user_platform=" + encodeURIComponent(Browser.platformCaps.join(",")));
export function getPublicApiUrl(path: string) : string {
//getServiceUrl() + "/api/" + path;
return appendAccessToken(getServiceUrl() + "/api/" + path);
}
export function getPrivateApiUrl(path: string) : string {
return appendAccessToken(getServiceUrl() + "/api" + (path == null ? "" : "/" + path));
}
export function getScriptTextAsync(id: string) : Promise {
return Util.httpGetTextAsync(getPublicApiUrl(encodeURIComponent(id) + "/text?original=true&ids=true"))
}
export function getPrivateApiAsync(path: string) : Promise {
return Util.httpGetJsonAsync(getPrivateApiUrl(path));
}
export function getPublicApiAsync(path: string) : Promise {
return Util.httpGetJsonAsync(getPublicApiUrl(path));
}
export function postPrivateApiAsync(path:string, req:any) : Promise {
return Util.httpPostJsonAsync(getPrivateApiUrl(path), req);
}
export function deletePrivateApiAsync(path: string): Promise {
return Util.httpRequestAsync(Cloud.getPrivateApiUrl(path), "DELETE");
}
export function deletePublicationAsync(id: string): Promise {
return Util.httpRequestAsync(Cloud.getPrivateApiUrl(id), "DELETE");
}
export function getRandomAsync() : Promise {
return Util.httpGetTextAsync(getPublicApiUrl("random"));
}
export interface Version {
instanceId: string;
version: number;
time: number;
// LITE
baseSnapshot: string;
}
export function isVersionNewer(version1: Version, version2: Version): boolean {
if (typeof version1 === "object" && typeof version2 === "object")
{
if (version1.instanceId == version2.instanceId)
return version1.version > version2.version || version1.version == version2.version && version1.time > version2.time;
else
return version1.time > version2.time;
}
return false;
}
export interface Header {
guid: string;
name: string;
scriptId: string;
scriptTime:number;
updateId: string;
updateTime:number;
scriptVersion: Version;
meta: any;
capabilities: string;
flow: string;
sourcesThatNeedToBeGrantedAccess: string;
userId: string;
status: string;
hasErrors: boolean;
//libraryDependencies: string[];
publishAsHidden:boolean;
recentUse: number; // seconds since epoch
// For compatibility reasons with previous cloud entries, we need to
// adopt the view that [editor == undefined] means "default
// TouchDevelop" editor, while anything else means "external editor".
editor?: string;
}
export interface AskSomething {
title: string;
picture?: string;
message: string;
linkName?: string;
linkUrl?: string;
}
export interface InstalledHeaders {
headers: Header[];
newNotifications: number;
notifications: boolean;
email: boolean;
emailNewsletter: boolean;
emailNotifications: boolean;
profileIndex: number;
profileCount: number;
time: number;
askBeta?:boolean;
askSomething?:AskSomething;
minimum?: string;
random?:string;
v?: number;
user?: any;
blobcontainer?: string;
}
export interface InstalledBodies {
bodies: Body[];
recentUses: RecentUse[];
}
export interface UserSettings {
nickname?: string;
aboutme?: string;
website?: string;
notifications?: boolean;
notifications2?: string;
picturelinkedtofacebook?: boolean;
realname?: string;
gender?: string;
howfound?: string;
culture?: string;
yearofbirth?: number;
programmingknowledge?: string;
occupation?: string;
emailnewsletter2?: string;
emailfrequency?: string;
email?: string;
location?: string;
twitterhandle?: string;
editorMode?: string;
school?: string;
wallpaper?: string;
}
export function getUserInstalledAsync() : Promise // of InstalledHeaders
{
return getPrivateApiAsync("me/installed");
}
export function getUserInstalledLongAsync(v?: number, m?: boolean) : Promise // of InstalledHeaders
{
return getPrivateApiAsync("me/installedlong" + (v ? "?v=" + v + (m ? "&m=1" : "") : ""));
}
export interface RecentUse {
guid: string;
recentUse: number; // seconds since epoch
}
// See the [Header] type for more comments.
export interface Body {
guid: string;
name: string;
scriptId: string;
updateId: string;
scriptVersion: Version;
meta: string;
capabilities: string;
flow: string;
sourcesThatNeedToBeGrantedAccess: string;
userId: string;
status: string;
hasErrors: boolean;
//libraryDependencies: string[];
script: string;
editorState: string;
recentUse: number; // seconds since epoch
editor?: string;
}
export interface BatchResponse
{
code: number;
body: any;
ETag: string;
}
export interface BatchResponses
{
code: number;
array: BatchResponse[];
}
export interface PostUserInstalledResponse {
v?: number;
delay: number;
numErrors?: number;
}
2015-02-05 00:44:02 +03:00
export interface PostApiGroupsBody {
name: string;
description: string;
school?:string;
grade?:string;
2015-02-05 00:41:44 +03:00
allowexport: boolean;
allowappstatistics: boolean;
2015-02-05 00:44:02 +03:00
userplatform: string[];
}
export interface PostApiGroupsResponse {
id: string;
}
export interface ApiGroupCodeResponse {
code: string; // can be null; in particular, is null initially
2015-02-05 00:41:44 +03:00
expiration: number; // can be null; in particular, is null initially; in seconds since 1970
}
export interface ApiGroupCodeRequest {
2015-02-05 00:44:02 +03:00
expiration?: number;
// in seconds since 1970; if present, cannot be in the past or more than a year in the future;
// defaults to 14 days into the future if null or not present
}
2015-02-05 00:41:44 +03:00
export function getUserInstalledBodyAsync(guid: string) : Promise // of InstalledBodies
{
return getPrivateApiAsync("me/installed/" + guid);
}
export function postUserInstalledAsync(installedBodies: InstalledBodies) : Promise // of PostUserInstalledResponse
{
return Util.httpPostJsonAsync(getPrivateApiUrl("me/installed"), installedBodies);
}
export function postUserInstalledPublishAsync(guid:string, hidden:boolean, scriptVersion:string, meta?:any) : Promise // of InstalledBodies
{
var url = "me/installed/" + guid + "/publish?hidden=" + (hidden ? "true" : "false")
if (scriptVersion)
url += "&scriptversion=" + encodeURIComponent(scriptVersion)
if (!meta) meta = {}
var mergeIds = meta.parentIds
if (mergeIds)
url += "&mergeids=" + encodeURIComponent(mergeIds)
return Util.httpPostJsonAsync(getPrivateApiUrl(url), Cloud.lite ? meta : "")
}
export function postApiBatch(bundle: any) : Promise // of BatchResponses
{
return Util.httpPostJsonAsync(getPrivateApiUrl(null), bundle);
}
export function postBugReportAsync(bug: BugReport) : Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl("bug"), bug);
}
export function postRunReportAsync(id: string, run: any): Promise // of ???
{
return Util.httpPostJsonAsync(getPrivateApiUrl(id + "/runs"), run);
}
export function postTicksAsync(ticks:any) : Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl("ticks"), ticks);
}
export function postNotificationsAsync() : Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl("me/notifications"), "");
}
2015-02-05 00:44:02 +03:00
export interface PushNotificationRequestBody {
// Push notification URL;
// our cloud code will recognize by the URL what the target is. The URL must be understood by System.Uri.TryCreate
subscriptionuri: string;
versionminor: number; // minor OS version, e.g. 0
versionmajor: number; // major OS version, e.g. 4
}
2015-02-05 00:41:44 +03:00
export function postNotificationChannelAsync(body: PushNotificationRequestBody) : Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl("me/notificationchannel"), body);
}
export function getUserApiKeysAsync(): Promise {
return Util.httpGetJsonAsync(getPrivateApiUrl("me/keys"));
}
export function getUserSettingsAsync(): Promise {
return Util.httpGetJsonAsync(getPrivateApiUrl("me/settings"));
}
export function postUserSettingsAsync(body: UserSettings) : Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl("me/settings"), body);
}
export interface AppApiKey
{
id : string;
name : string;
url : string;
help: string;
value : string;
}
export function getAppAsync(id:string, appPlatform : string) : Promise // of json
{
return Util.httpGetJsonAsync(getPrivateApiUrl(id + "/" + appPlatform + "app"));
}
export function postAppAsync(id:string, appPlatform : string, data:any) : Promise // of string
{
return Util.httpPostTextAsync(getPrivateApiUrl(id + "/" + appPlatform + "app"), JSON.stringify(data));
}
export function getWebAppAsync(id:string) : Promise // of json
{
return Util.httpGetJsonAsync(getPrivateApiUrl(id + "/webapp"));
}
export function postWebAppAsync(id: string, previewUrl: boolean, data: any): Promise // of string
{
return Util.httpPostTextAsync(getPrivateApiUrl(id + "/webapp" + (previewUrl ? "?previewUrl=true" : "")), JSON.stringify(data));
}
export function deleteWebAppAsync(id: string): Promise // of string
{
return Util.httpDeleteAsync(getPrivateApiUrl(id + "/webapp"));
}
export function postAskBetaAsync(accept:boolean) : Promise // of string
{
return Util.httpPostTextAsync(getPrivateApiUrl("/me/askbeta?accept=" + accept), "");
}
export function postAskSomethingAsync(accept: boolean): Promise // of string
{
return Util.httpPostTextAsync(getPrivateApiUrl("/me/asksomething?accept=" + accept), "");
}
2015-02-05 00:44:02 +03:00
// ping the server to test if it is online
// and there is no funny filtering happening
// this is costly so needs to be used wisely
2015-02-05 00:41:44 +03:00
export function pingAsync(): Promise // of boolean
{
if (/http:\/\/localhost/i.test(document.URL)) return Promise.as(true); // does not work for localhost
var v = TDev.RT.Math_.random(0xffffff).toString();
var url = getPublicApiUrl("ping?value=" + encodeURIComponent(v));
return new Promise((onSuccess: (v: any) => any, onError: (v: any) => any, onProgress: (v: any) => any) => {
var client: XMLHttpRequest;
function ready() {
if (client.readyState == 4)
onSuccess(client.status == 200 && client.responseText === v);
}
client = new XMLHttpRequest();
client.onreadystatechange = ready;
client.open("GET", url);
client.send();
});
}
export function postProfilingDataAsync(id: string, body: any /*ProfilingData*/): Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl(id + "/profile?v=1"), body);
}
export function postCoverageDataAsync(id: string, body: any /*CoverageData*/): Promise // of void
{
return Util.httpPostJsonAsync(getPrivateApiUrl(id + "/coverage"), body);
}
export function getCoverageDataAsync(id: string, compilerversion: string, intersection: boolean = false): Promise // of CoverageData[]
{
return Util.httpGetJsonAsync(getPrivateApiUrl(id + "/coverage?compilerversion=" + encodeURIComponent(compilerversion) + (intersection ? "&view=intersection" : "")));
}
export interface Covered {
count: number;
}
export function getCoveredAsync(id: string, compilerversion: string): Promise // of Covered
{
return Util.httpGetJsonAsync(getPrivateApiUrl("me/covered/" + id + "?compilerversion=" + encodeURIComponent(compilerversion)));
}
export function getProfileDataAsync(id: string, compilerversion: string): Promise // of ProfileData[]
{
return Util.httpGetJsonAsync(getPrivateApiUrl(id + "/profile?compilerversion=" + encodeURIComponent(compilerversion)));
}
export interface Profiled {
count: number;
}
export function getProfiledAsync(id: string, compilerversion: string): Promise // of Profiled
{
return Util.httpGetJsonAsync(getPrivateApiUrl("me/profiled/" + id + "?compilerversion=" + encodeURIComponent(compilerversion)));
}
export function postPendingProgressAsync() {
if (!getUserId() || !getAccessToken() || isOffline() || dbg) return Promise.as();
var data = loadPendingProgress();
if (Object.keys(data).length == 0) return Promise.as();
return Cloud.postPrivateApiAsync("me/progress", data)
.then(
() => clearPendingProgress(data),
() => { }); // clear relevant progress records on success, otherwise swallow error
}
export function postCommentAsync(id: string, text:string): Promise { // JsonComment
var req = { kind: "comment", text: text, userplatform: Browser.platformCaps };
return Cloud.postPrivateApiAsync(id + "/comments", req)
}
}
}