├── .babelrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── backup-script ├── backup.js ├── cron.sh ├── go.sh └── package.json ├── build.sh ├── dockerbuild ├── .dockerignore ├── Dockerfile ├── README.md ├── builder.js ├── go.js ├── package.json ├── req.json ├── run └── server.js ├── docs ├── README.md ├── REST-APIs.md ├── deployment.md ├── map.md ├── ssl.md └── streams.md ├── external └── backendutils.js ├── jsconfig.json ├── package.json ├── scripts ├── asynclint.js └── mkaccounts.sh ├── shell ├── Makefile ├── azure │ ├── build.cmd │ ├── files.txt │ ├── setup_worker.cmd │ ├── showlog.js │ ├── start_worker.cmd │ └── tdshell.csdef ├── shell.ts └── tsconfig.json ├── src ├── Makefile ├── acs.ts ├── azure-blob-storage.ts ├── azure-search.ts ├── azure-table.ts ├── cached-store.ts ├── cron.ts ├── crowdin.ts ├── indexed-store.ts ├── kraken.ts ├── librato-node.ts ├── loggly.ts ├── mbedworkshop-compiler.ts ├── microsoft-translator.ts ├── nodemailer.ts ├── parallel.ts ├── raygun.ts ├── redis.ts ├── remote.ts ├── restify.ts ├── sendgrid.ts ├── server-auth.ts ├── storutil.ts ├── td.ts ├── tdlite-abuse.ts ├── tdlite-admin.ts ├── tdlite-art.ts ├── tdlite-audit.ts ├── tdlite-channels.ts ├── tdlite-comments.ts ├── tdlite-core.ts ├── tdlite-counters.ts ├── tdlite-cppcompiler.ts ├── tdlite-crashes.ts ├── tdlite-data.ts ├── tdlite-docs.ts ├── tdlite-groups.ts ├── tdlite-html.ts ├── tdlite-i18n.ts ├── tdlite-import.ts ├── tdlite-index.ts ├── tdlite-legacy.ts ├── tdlite-login.ts ├── tdlite-notifications.ts ├── tdlite-pointers.ts ├── tdlite-progress.ts ├── tdlite-promos.ts ├── tdlite-releases.ts ├── tdlite-reviews.ts ├── tdlite-routing.ts ├── tdlite-runtime.ts ├── tdlite-scripts.ts ├── tdlite-search.ts ├── tdlite-status.ts ├── tdlite-streams.ts ├── tdlite-tags.ts ├── tdlite-tdcompiler.ts ├── tdlite-ticks.ts ├── tdlite-users.ts ├── tdlite-vimeo.ts ├── tdlite-workspace.ts ├── tdlite.ts ├── tdshell.ts ├── templater.ts ├── test-service.ts └── word-password.ts ├── tsconfig.json ├── tsd.json ├── typings ├── bluebird │ └── bluebird.d.ts ├── marked │ └── marked.d.ts └── node │ └── node.d.ts └── web ├── _includes ├── footer.html ├── head.html ├── html-foot.html └── topnav.html ├── _layouts ├── bare.html ├── default.html ├── docs.html └── signin.html ├── _mockup ├── contents.html ├── publishing.html └── var.html ├── app └── authorize.html ├── appday.html ├── blog.html ├── ccga.html ├── contact.redir.txt ├── doc ├── touchdevelop114x114-png.redir.txt ├── touchdevelop144x144-png.redir.txt ├── touchdevelop1511x1511-png.redir.txt ├── touchdevelop196x196-png.redir.txt ├── touchdevelop200x200-png.redir.txt ├── touchdevelop512x512-png.redir.txt ├── touchdevelop57x57-png.redir.txt └── touchdevelop72x72-png.redir.txt ├── error-template.html ├── home.html ├── hourofcode2.html ├── i18n-messages.html ├── legal.redir.txt ├── microbit.html ├── microbituploader-exe.redir.txt ├── microbituploader-zip.redir.txt ├── microbituploader.html ├── platforms.html ├── privacy.redir.txt ├── signin ├── emailcode.html ├── legacy.html ├── legacylogin.html └── providers.html ├── signout.html ├── static ├── css │ ├── style.css │ └── touchdevelop.css ├── images │ ├── aaat.png │ ├── aaat2.png │ ├── aakr.png │ ├── amgj.png │ ├── avpewfrs.png │ ├── blockeditor.png │ ├── botinship_open.png │ ├── checker.png │ ├── cvme.png │ ├── cxld.png │ ├── cyftmmbj.png │ ├── enmn.png │ ├── geqc.png │ ├── gkcw.png │ ├── html5.png │ ├── hwkuanhf.png │ ├── izmtgprs.png │ ├── kzajxznr.png │ ├── login-facebook.png │ ├── login-google.png │ ├── login-microsoft.png │ ├── login-office365.png │ ├── login-yahoo.png │ ├── makecodemc.jpg │ ├── microbit.uploader.screenshot.png │ ├── microbit.uploader.uploading.png │ ├── mwpv.png │ ├── play.png │ ├── qlgq.png │ ├── seie.png │ ├── sftkyutq.png │ ├── sgic.png │ ├── stegmfdr.png │ ├── toplogo.png │ ├── touchdevelop.png │ ├── user.png │ ├── uwzvapre.png │ ├── wdgn.png │ ├── wlmemodx.png │ ├── yvco.png │ └── ywydysip.png ├── js │ ├── docs.js │ ├── home.js │ ├── mscc.js │ ├── social.js │ └── utils.js ├── logo │ ├── blockeditor.png │ ├── blockeditorpink.png │ ├── logo114.png │ ├── logo128.png │ ├── logo144.png │ ├── logo1511.png │ ├── logo196.png │ ├── logo200.png │ ├── logo48.png │ ├── logo512.png │ ├── logo57.png │ ├── logo72.png │ ├── touchdevelop.png │ └── touchdevelopblue.png ├── minecraft │ └── touchdevelopmod.zip └── uploader │ ├── Microbit.Uploader.exe │ └── Microbit.Uploader.zip ├── templates ├── docs.html ├── docscript.html ├── oauth.html ├── official.html ├── script.html ├── tutorial.html └── users.html ├── touchdevelopmod-zip.redir.txt ├── trademarks.redir.txt └── worker-js.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-classes", 4 | "transform-es2015-destructuring", 5 | "transform-es2015-object-super", 6 | "transform-es2015-parameters", 7 | "transform-es2015-spread" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # enforce unix style line endings 2 | *.ts text eol=lf 3 | *.tsx text eol=lf 4 | *.md text eol=lf 5 | *.txt text eol=lf 6 | *.js text eol=lf 7 | *.json text eol=lf 8 | *.xml text eol=lf 9 | *.svg text eol=lf 10 | *.yaml text eol=lf 11 | *.css text eol=lf 12 | *.html text eol=lf 13 | *.py text eol=lf 14 | *.exp text eol=lf 15 | 16 | # do not enforce text for everything - it causes issues with random binary files 17 | 18 | *.sln text eol=crlf 19 | 20 | *.png binary 21 | *.jpg binary 22 | *.jpeg binary 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | node_modules/ 3 | built/ 4 | remote-* 5 | storage-* 6 | web/_site/ 7 | templater.sh 8 | web/_temp 9 | dockerbuild/config.json 10 | dockerbuild/yottaconfig.json 11 | localdir/ 12 | script.json 13 | mirror/ 14 | package-lock.json 15 | tmp 16 | 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.0.0" 4 | before_script: 5 | - "tsd reinstall" 6 | sudo: false 7 | script: 8 | - "make" 9 | after_success: 10 | - "make upload" 11 | notifications: 12 | email: 13 | - touchdevelop-build@microsoft.com 14 | cache: 15 | directories: 16 | - node_modules 17 | - typings 18 | - web/_temp 19 | 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Third Party Programs: The software may include third party programs that 24 | Microsoft, not the third party, licenses to you under this agreement. 25 | Notices, if any, for the third party programs are included for your 26 | information only. 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | N=tdlite 2 | 3 | all: 4 | sh ./build.sh 5 | 6 | docs: all 7 | PORT=4000 node built/templater.js serve 8 | 9 | # requires TD_UPLOAD_KEY 10 | upload: 11 | if [ "X$$TRAVIS_BRANCH" = "Xmaster" ] ; then node built/templater.js push ; fi 12 | 13 | pxt: 14 | make -C ../pxt 15 | cp ../pxt/built/backendutils.js external/ 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TouchDevelop backend 2 | 3 | Back-end cloud service for [Touch Develop] (https://github.com/Microsoft/TouchDevelop) 4 | 5 | [![Build Status](https://travis-ci.org/Microsoft/TouchDevelop.svg)](https://travis-ci.org/Microsoft/TouchDevelop) 6 | 7 | ## Setup 8 | 9 | In order to build Touch Develop backend, ensure that you have 10 | [Git](http://git-scm.com/downloads) and [Node.js](http://nodejs.org/) 11 | installed. 12 | 13 | [Visual Studio Code] (https://code.visualstudio.com/), a free cross-platform 14 | IDE, is useful for editing (but not required). Settings files are checked in. 15 | 16 | Clone a copy of the repo: 17 | 18 | ``` 19 | git clone https://github.com/Microsoft/TouchDevelop-backend.git 20 | ``` 21 | 22 | ``` 23 | cd TouchDevelop-backend 24 | npm install tsd@next -g 25 | tsd reinstall 26 | npm install 27 | make 28 | ``` 29 | 30 | ## Web stuff 31 | 32 | `make docs` will start a server at http://localhost:4000 33 | 34 | ## Updating dependencies 35 | 36 | azure-storage should stay at 0.6.0 unless great care is take to update 37 | azure-blob-storage.ts (there are breaking changes and we're using `_putBlockBlob`) 38 | 39 | ## LICENSE 40 | 41 | The MIT License (MIT) 42 | 43 | Copyright (c) 2016 Microsoft 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy 46 | of this software and associated documentation files (the "Software"), to deal 47 | in the Software without restriction, including without limitation the rights 48 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 49 | copies of the Software, and to permit persons to whom the Software is 50 | furnished to do so, subject to the following conditions: 51 | 52 | The above copyright notice and this permission notice shall be included in all 53 | copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 56 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 57 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 58 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 59 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 60 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 61 | SOFTWARE. 62 | -------------------------------------------------------------------------------- /backup-script/cron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd $HOME/bkp 3 | node backup.js < envbkp.json > lastcron.txt 2>&1 4 | -------------------------------------------------------------------------------- /backup-script/go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | node backup.js < envbkp.json 3 | -------------------------------------------------------------------------------- /backup-script/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backup-script", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Backup script for Touch Develop backend", 6 | "main": "backup.js", 7 | "author": "Michal Moskal ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "async": "^1.4.2", 11 | "azure-storage": "^0.6.0", 12 | "azure-table-node": "^1.4.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | mkdir -p built/ 5 | cp external/backendutils.js built/ 6 | node scripts/asynclint.js src/*.ts 7 | echo "[tsc]" 8 | node node_modules/typescript/bin/tsc 9 | echo "[babel]" 10 | node node_modules/babel-cli/bin/babel -q built/ts --out-dir built 11 | echo "[done]" 12 | -------------------------------------------------------------------------------- /dockerbuild/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /dockerbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Pull base image 3 | FROM mbed/yotta 4 | MAINTAINER Michal Moskal 5 | 6 | RUN apt-get install rlwrap 7 | 8 | RUN curl https://deb.nodesource.com/node_4.x/pool/main/n/nodejs/nodejs_4.4.3-1nodesource1~trusty1_amd64.deb > node.deb \ 9 | && dpkg -i node.deb \ 10 | && rm node.deb 11 | 12 | RUN apt-get install -y srecord 13 | 14 | RUN useradd -m build 15 | 16 | COPY go.js /home/build 17 | -------------------------------------------------------------------------------- /dockerbuild/README.md: -------------------------------------------------------------------------------- 1 | # Docker-based build scripts 2 | 3 | ## Setup 4 | 5 | In `/etc/fstab` add: 6 | 7 | ``` 8 | tmpfs /docker tmpfs rw,size=4G,nodev,noatime,mode=1700 0 0 9 | ``` 10 | 11 | Create `/etc/systemd/system/docker.service` with the following: 12 | 13 | ``` 14 | [Service] 15 | ExecStart= 16 | ExecStart=/usr/bin/docker daemon -H fd:// -g /docker 17 | ``` 18 | 19 | Then do: 20 | 21 | ``` 22 | cd 23 | sudo su - 24 | mkdir /docker 25 | mount -a 26 | systemctl daemon-reload 27 | systemctl restart docker 28 | docker pull pext/yotta 29 | ``` 30 | 31 | Install node.js (on host, not in docker): 32 | ``` 33 | curl -sL https://deb.nodesource.com/setup_4.x | bash - 34 | apt-get install nodejs 35 | ``` 36 | 37 | Clone TD-backend repo: 38 | 39 | ``` 40 | sudo su - build 41 | git clone https://github.com/Microsoft/TouchDevelop-backend.git 42 | cd TouchDevelop-backend/dockerbuild 43 | npm install 44 | ``` 45 | 46 | Copy `.yotta/config.json` with the right credentials to `yottaconfig.json`: 47 | 48 | ``` 49 | scp yottaconfig.json build@somewhere.cloudapp.net:TouchDevelop-backend/dockerbuild 50 | ``` 51 | 52 | Run `node server.js` and create `config.json` file as prompted. You can use the key it generates. 53 | 54 | Then run the server in screen session. 55 | ``` 56 | screen 57 | node server.js 58 | Ctrl-A Ctrl-D 59 | ``` 60 | 61 | You can login later into the machine, you can do `screen -r` to re-attach the screen and see what's going on. 62 | -------------------------------------------------------------------------------- /dockerbuild/builder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs") 4 | var path = require("path") 5 | var child_process = require("child_process") 6 | 7 | process.stdout.setEncoding("utf8") 8 | 9 | 10 | function mkdirP(thePath) { 11 | if (thePath == ".") return; 12 | if (!fs.existsSync(thePath)) { 13 | mkdirP(path.dirname(thePath)) 14 | fs.mkdirSync(thePath) 15 | } 16 | } 17 | 18 | function handle(req) { 19 | if (req.empty && !req.buildpath) 20 | req.buildpath = "/home/build/prj2" 21 | let rootdir = req.buildpath || "/home/build/microbit-touchdevelop"; 22 | if (!fs.existsSync(rootdir)) 23 | fs.mkdirSync(rootdir) 24 | process.chdir(rootdir); 25 | let modulejson = ""; 26 | (req.files || []).forEach(function(f) { 27 | if (f.name == "module.json") modulejson = f.text; 28 | }) 29 | if (modulejson) { 30 | fs.writeFileSync("module.json", modulejson) 31 | } 32 | let cmd = `git pull --tags && git checkout ${req.gittag} && yotta update` 33 | if (req.empty) 34 | cmd = `yotta target ${req.target} && yotta update` 35 | if (req.yottaconfig) { 36 | mkdirP(process.env["HOME"] + "/.yotta") 37 | fs.writeFileSync(process.env["HOME"] + "/.yotta/config.json", JSON.stringify(req.yottaconfig, null, 4)) 38 | } 39 | let res = child_process.spawnSync("bash", ["-c", cmd], { encoding: "utf8" }) 40 | let resp = { 41 | stdout: res.stdout || "", 42 | stderr: res.stderr || "", 43 | status: res.status, 44 | } 45 | 46 | if (res.status == 0) { 47 | fs.unlinkSync(process.env["HOME"] + "/.yotta/config.json"); 48 | (req.files || []).forEach(function(f) { 49 | mkdirP(path.dirname(f.name)) 50 | fs.writeFileSync(f.name, f.text); 51 | }) 52 | res = child_process.spawnSync("yotta", ["build"], { encoding: "utf8" }); 53 | resp.stdout += res.stdout || "" 54 | resp.stderr += res.stderr || "" 55 | resp.status = res.status 56 | 57 | if (res.status == 0) { 58 | process.chdir("build") 59 | process.chdir(req.target || "bbc-microbit-classic-gcc") 60 | let hex = req.hexfile || "source/microbit-touchdevelop-combined.hex" 61 | if (fs.existsSync(hex)) 62 | resp.hexfile = fs.readFileSync(hex, "utf8") 63 | } 64 | } 65 | 66 | process.stdout.write(JSON.stringify(resp, null, 1)) 67 | process.stdout.write("\n") 68 | } 69 | 70 | handle(global.buildReq) 71 | -------------------------------------------------------------------------------- /dockerbuild/go.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs") 2 | var child_process = require("child_process") 3 | 4 | process.stdin.setEncoding("utf8") 5 | process.stdout.setEncoding("utf8") 6 | 7 | var buf = "" 8 | process.stdin.on("data", function(d) { buf += d }) 9 | process.stdin.on("end", function() { 10 | handle(JSON.parse(buf)) 11 | }) 12 | 13 | function handle(req) { 14 | fs.writeFileSync("builder.js", req.builderJs); 15 | global.buildReq = req; 16 | require("./builder") 17 | } 18 | -------------------------------------------------------------------------------- /dockerbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockerbuild", 3 | "version": "1.0.0", 4 | "private": true, 5 | "author": "", 6 | "license": "MIT", 7 | "dependencies": { 8 | "async": "^1.5.2", 9 | "azure-storage": "^0.8.0", 10 | "request": "^2.69.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /dockerbuild/req.json: -------------------------------------------------------------------------------- 1 | { 2 | "op": "buildex", 3 | "files": [ 4 | { 5 | "name": "module.json", 6 | "text": "{\n \"name\": \"pxt-microbit-app\",\n \"version\": \"0.0.0\",\n \"description\": \"Auto-generated. Do not edit.\",\n \"license\": \"n/a\",\n \"dependencies\": {\n \"pxt-microbit-core\": \"microsoft/pxt-microbit-core#v0.1.8\"\n },\n \"targetDependencies\": {},\n \"bin\": \"./source\"\n}\n" 7 | }, 8 | { 9 | "name": "config.json", 10 | "text": "{\n \"microbit-dal\": {\n \"bluetooth\": {\n \"enabled\": 0\n }\n }\n}\n" 11 | }, 12 | { 13 | "name": "source/main.cpp", 14 | "text": "#include \"pxt.h\"\nint main() { \n uBit.init(); \n pxt::start(); \n return 0; \n}\n" 15 | } 16 | ], 17 | "target": "bbc-microbit-classic-gcc", 18 | "hexfile": "source/pxt-microbit-app-combined.hex", 19 | "empty": true 20 | } 21 | -------------------------------------------------------------------------------- /dockerbuild/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | b=8b0c24a027fa4c041cab37d0368e8c9814bd50d5c3b937ebb000729af2660e33 4 | 5 | cmd="$1" 6 | if [ "X$cmd" = X ] ; then 7 | : 8 | else 9 | shift 10 | fi 11 | 12 | case $cmd in 13 | sh) 14 | if [ "X$1" = X ] ; then 15 | exit 1 16 | fi 17 | #docker run -w /build -u build -it $1 bash -l 18 | docker run -w /home/build -u build -it $1 bash -l 19 | ;; 20 | save) 21 | id=`docker ps -l | tail -1 | awk '{print $1}'` 22 | cid=`docker commit $id` 23 | docker save $cid | tar --delete $b | gzip > $cid.tgz 24 | docker rmi -f $cid 25 | ls -l $cid.tgz 26 | echo "IMGID $cid" 27 | ;; 28 | update) 29 | set -e 30 | docker run -i -u build $b bash -c "set -x; set -e; cat >/build/go.js; cd /build/microbit-touchdevelop; git checkout source/main.cpp; git pull --tags; $1 yt update; cp yotta_modules/microbit-dal/source/CortexContextSwitch.s{.gcc,}; yt build" < go.js 31 | $0 save 32 | ;; 33 | build) 34 | docker run -i -u build -w /build/microbit-touchdevelop $1 sh -c "cat >source/main.cpp; cd build/bbc-microbit-classic-gcc; ninja" 35 | ;; 36 | *) 37 | echo "Usage: $0 command" 38 | echo "Command is:" 39 | echo "sh -> run bash" 40 | echo "save -> save most recent container to tgz file" 41 | echo "update -> update yotta and save" 42 | ;; 43 | esac 44 | -------------------------------------------------------------------------------- /dockerbuild/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var http = require('http'); 4 | var child_process = require("child_process"); 5 | var crypto = require("crypto"); 6 | var fs = require("fs"); 7 | var domain = require("domain"); 8 | var async = require("async"); 9 | 10 | if (!fs.existsSync("config.json")) { 11 | var cfg = { 12 | key: crypto.randomBytes(32).toString("hex") 13 | }; 14 | console.log("Please create config.json, for example one like this:") 15 | console.log(JSON.stringify(cfg, null, 4)) 16 | process.exit(1) 17 | } 18 | 19 | 20 | if (!fs.existsSync("yottaconfig.json")) { 21 | console.log("Please copy ~/.yotta/config.json to yottaconfig.json") 22 | process.exit(1) 23 | } 24 | 25 | var cfg = JSON.parse(fs.readFileSync("config.json", "utf8")) 26 | var ycfg = JSON.parse(fs.readFileSync("yottaconfig.json", "utf8")) 27 | 28 | function err(res, code, msg) { 29 | console.log("err: " + code + ": " + msg) 30 | res.writeHead(code); res.end(msg); 31 | } 32 | 33 | var key = new Buffer(cfg.key, "hex") 34 | 35 | process.on('uncaughtException', function (err) { 36 | console.log(err); 37 | }) 38 | 39 | var builderJs = fs.readFileSync("builder.js", "utf8") 40 | 41 | var buildq = async.queue(function (f, cb) { f(cb) }, 8) 42 | function build(js, outp) { 43 | buildq.push(function (cb) { 44 | console.log("Build") 45 | var ch = child_process.spawn("docker", [ 46 | "run", "--rm", "-i", 47 | "-w", "/home/build", "-u", "build", 48 | js.image || "pext/yotta", 49 | "sh", "-c", "node go.js 2>&1"], 50 | {}) 51 | js.builderJs = builderJs 52 | js.yottaconfig = ycfg 53 | ch.stdin.write(JSON.stringify(js)) 54 | ch.stdin.end() 55 | ch.stdout.pipe(outp) 56 | 57 | ch.stderr.setEncoding("utf8") 58 | var remove = "" 59 | ch.stderr.on("data", function (d) { 60 | var m = /Cannot destroy container ([0-9a-f]{20,})/.exec(d) 61 | if (m) { 62 | remove = m[1] 63 | } else { 64 | console.log(d) 65 | } 66 | }) 67 | ch.on("exit", function () { 68 | cb(null) 69 | if (remove) 70 | setTimeout(function () { 71 | console.log("remove container " + remove) 72 | var ch = child_process.spawn("docker", ["rm", remove]) 73 | ch.stderr.pipe(process.stderr) 74 | }, 1000) 75 | }) 76 | }) 77 | } 78 | 79 | function handleReq(req, res) { 80 | console.log(req.url) 81 | var iv = req.headers["x-iv"] 82 | if (!iv) { 83 | err(res, 403, "No iv") 84 | return 85 | } 86 | iv = new Buffer(iv.replace(/\s+/g, ""), "hex") 87 | if (!iv || iv.length != 16) { 88 | err(res, 403, "bad iv") 89 | return 90 | } 91 | 92 | var ciph = crypto.createDecipheriv("AES256", key, iv) 93 | ciph.setEncoding("utf8") 94 | var dd = "" 95 | ciph.on("data", function (d) { dd += d }) 96 | ciph.on("end", function () { 97 | if (dd.length < 128) { 98 | err(res, 403, "too short") 99 | return 100 | } 101 | try { 102 | var js = JSON.parse(dd) 103 | } catch (e) { 104 | err(res, 403, "bad key") 105 | return 106 | } 107 | var oiv = crypto.randomBytes(16) 108 | res.setHeader("x-iv", oiv.toString("hex")) 109 | var enciph = crypto.createCipheriv("AES256", key, oiv) 110 | enciph.pipe(res); 111 | res.writeHead(200); 112 | 113 | if (js.op == "buildex") 114 | build(js, enciph) 115 | else 116 | enciph.end(JSON.stringify({ err: "Wrong OP" })) 117 | }) 118 | req.pipe(ciph) 119 | } 120 | 121 | if (process.argv[2]) { 122 | let f = fs.readFileSync(process.argv[2], "utf8"); 123 | build(JSON.parse(f), process.stdout) 124 | } else { 125 | http.createServer(function (req, res) { 126 | var d = domain.create(); 127 | d.on('error', function (er) { 128 | console.error('error', er.stack); 129 | try { 130 | res.statusCode = 500; 131 | res.setHeader('content-type', 'text/plain'); 132 | res.end(); 133 | } catch (e) { 134 | } 135 | }) 136 | d.add(req); 137 | d.add(res); 138 | d.run(function () { handleReq(req, res) }) 139 | 140 | }).listen(2424); 141 | } 142 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # TouchDevlop-backend docs 2 | 3 | TouchDevelop-backend (referred to as TDB in the rest of this document) 4 | serves: 5 | * https://www.touchdevelop.com 6 | * https://www.microbit.co.uk 7 | * https://pxt.io (including https://codemicrobit.com) 8 | 9 | TDB is written entirely in TypeScript and runs using node.js runtime in Azure 10 | classic cloud services. 11 | 12 | Following docs are available: 13 | 14 | * [shell and remote.js](deployment.md) 15 | * [generating SSL certs](ssl.md) 16 | * [general source architecture](map.md) 17 | * [REST APIs description](REST-APIs.md) 18 | * [Streams APIs description](streams.md) 19 | 20 | -------------------------------------------------------------------------------- /docs/ssl.md: -------------------------------------------------------------------------------- 1 | # SSL certificates for TDB 2 | 3 | ## Generating SSL certificates 4 | 5 | Create file `td.config` like this: 6 | 7 | 8 | ``` 9 | [ req ] 10 | x509_extensions = v3_req 11 | distinguished_name = req_distinguished_name 12 | req_extensions = v3_req 13 | prompt = no 14 | 15 | [ req_distinguished_name ] 16 | countryName = US 17 | stateOrProvinceName = Washington 18 | localityName = Redmond 19 | 0.organizationName = Microsoft 20 | organizationalUnitName = Microsoft Research 21 | commonName = www.pxt.io 22 | 23 | [ v3_req ] 24 | basicConstraints = CA:FALSE 25 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 26 | 27 | subjectAltName = @alt_names 28 | 29 | [ alt_names ] 30 | 31 | DNS.1 = pxt.io 32 | DNS.2 = *.pxt.io 33 | DNS.3 = userpxt.io 34 | DNS.3 = *.userpxt.io 35 | ... 36 | ``` 37 | 38 | Run: 39 | 40 | ```bash 41 | openssl req -new -newkey rsa:2048 -nodes -sha256 -config td.config -days 730 -keyout td.key -out td.csr 42 | ``` 43 | 44 | Make sure you keep `td.key` secure. `td.csr` isn't confidential and will be 45 | required by whoever generate the certificate. 46 | 47 | Once you get the certificate, you need to create a password-less `.pfx` file, 48 | which contains the key, your certificate, and all certificates between your 49 | certificate and root (root is optional). Without the upper level 50 | certificates, the service will work in desktop Chrome and IE, but not in 51 | Firefox, or on Android. 52 | 53 | For example, if you have a `mycert.p7b` file with all needed certificates, do the 54 | following: 55 | 56 | ``` 57 | openssl pkcs7 -print_certs -in mycert.p7b -out cert.cer 58 | openssl pkcs12 -export -in cert.cer -inkey td.key -out cert.pfx 59 | node -p 'require("fs").readFileSync("cert.pfx").toString("base64")' > cert.pfx.b64 60 | ``` 61 | 62 | After first step you may want to remove the root certificate from `cert.cer` 63 | if it's there. This will make the certificate slightly smaller. 64 | 65 | The contents of `cert.pfx.b64` is what you need to put int `TD_HTTPS_PFX`. 66 | The `.pfx` file contains key and therefore needs to be kept secure. 67 | 68 | 69 | -------------------------------------------------------------------------------- /docs/streams.md: -------------------------------------------------------------------------------- 1 | # Streaming data API 2 | 3 | ## Creating a stream 4 | 5 | ``` 6 | POST /api/streams 7 | { 8 | "name": "My stream", 9 | "target": "microbit" 10 | } 11 | ``` 12 | 13 | The name and target are optional. Both are used when rendering the stream page at 14 | `/lxmsmqjlwucg`. The response will look like this (in JSON): 15 | 16 | ```json 17 | { 18 | "kind": "stream", 19 | "id": "lxmsmqjlwucg", 20 | "time": 1461862043, 21 | "name": "My stream", 22 | "target": "microbit", 23 | "meta": { 24 | "fields": [], 25 | "size": 0, 26 | "rows": 0, 27 | "batches": 0 28 | }, 29 | "privatekey": "XPbHZzTsBiimirjvVgVlzmIX" 30 | } 31 | ``` 32 | 33 | ## Posting data to a stream 34 | 35 | ``` 36 | POST /api/lxmsmqjlwucg/data?privatekey=XPbHZzTsBiimirjvVgVlzmIX 37 | { 38 | "fields": ["timestamp", "temp"], 39 | "values": [ 40 | [1461859756478, 20.5], 41 | [1461859756078, 21.22] 42 | ] 43 | } 44 | ``` 45 | 46 | The format is somewhat similar to CSV - you specify the "header" with `fields` 47 | and then specify one or more rows of values (samples) in `values`. 48 | 49 | The first field has to be `timestamp` and specify the time when the sample was 50 | taken as number of milliseconds since epoch (Jan 1st 1970). This is what JavaScript 51 | `Date.getTime()` method returns. 52 | 53 | All `fields` and each row of `values` has to have the same number of element. 54 | Use `null` to indicate no value. Field names have to be at most 60 characters 55 | and have to match `/^[a-zA-Z][a-zA-Z0-9_]*$/`. Values can be numbers or `null`. 56 | 57 | The response is: 58 | 59 | ```json 60 | { 61 | "meta": { ... }, 62 | "quotaUsedHere": 46, 63 | "quotaLeft": 52428708 64 | } 65 | ``` 66 | 67 | `meta` field is the same as below. `quotaUsedHere` is the amount of per-stream quota 68 | consumed by the post, and `quotaLeft` is amount of quoata remaining. 69 | 70 | ## Deleting stream 71 | 72 | ``` 73 | DELETE /api/lxmsmqjlwucg?privatekey=XPbHZzTsBiimirjvVgVlzmIX 74 | ``` 75 | 76 | This is irevocable and actually deletes the data. 77 | 78 | ## Querying stream meta info 79 | 80 | ``` 81 | GET /api/lxmsmqjlwucg 82 | ``` 83 | 84 | Response: 85 | 86 | ```json 87 | { 88 | "kind": "stream", 89 | "id": "lxmsmqjlwucg", 90 | "time": 1461862043, 91 | "name": "My stream", 92 | "meta": { 93 | "fields": [ 94 | { 95 | "name": "timestamp", 96 | "sum": 5847439025112, 97 | "min": 1461859756078, 98 | "max": 1461859756478, 99 | "count": 4 100 | }, 101 | { 102 | "name": "temp", 103 | "sum": 83.44, 104 | "min": 20.5, 105 | "max": 21.22, 106 | "count": 4 107 | } 108 | ], 109 | "size": 92, 110 | "rows": 4, 111 | "batches": 2 112 | } 113 | } 114 | ``` 115 | 116 | `time` is creation time (in seconds, not milliseconds, since epoch). 117 | `meta` specifies various stream information - the quota size of this 118 | stream, the number of rows and the number of post operations used 119 | to create it (`batches`). It also lists all the fields in the stream 120 | with their total minimum, maximum, number of times they were supplied 121 | in a row (this never greater than `rows` but can be smaller if some 122 | samples omit this field). There is also sum of values of this field, 123 | which lets you compute the average. Count on `timestamp` field is always the 124 | same as `rows`. 125 | 126 | ## Querying stream data 127 | 128 | This will query samples from three hours from now, until now. You can also 129 | use absolute time in `start` and `stop` (milliseconds from epoch). 130 | Acceptable units are `-1s, -1m, -1h, -1d, -1y`. It always has to be `-`. 131 | 132 | If there is more data to return, the `continuationUrl` will give you a URL 133 | where to look for it. You can also append `?continuation=` 134 | to your query URL. 135 | 136 | ``` 137 | GET /api/lxmsmqjlwucg/data?start=-3h&stop=-0s 138 | ``` 139 | 140 | ```json 141 | { 142 | "fields": [ 143 | { 144 | "name": "timestamp", 145 | "sum": 5847439025112, 146 | "min": 1461859756078, 147 | "max": 1461859756478, 148 | "count": 4 149 | }, 150 | { 151 | "name": "temp", 152 | "sum": 83.44, 153 | "min": 20.5, 154 | "max": 21.22, 155 | "count": 4 156 | } 157 | ], 158 | "values": [ 159 | [ 1461859756078, 21.22 ], 160 | [ 1461859756079, 21.22 ], 161 | [ 1461859756478, 20.5 ], 162 | [ 1461859756480 20.5 ] 163 | ], 164 | "continuation": "", 165 | "continuationUrl": "" 166 | } 167 | ``` 168 | 169 | Instead of `.../data` you can use `.../data.csv` to get data in CSV format. The CSV data 170 | omits partition (as it's the same in the entire file), and converts timestamp field 171 | into a UTC time representation that Excel understands. 172 | 173 | ## OData 174 | 175 | It's best avoided. 176 | 177 | There is some OData support at `/api/lxmsmqjlwucg/odata/` and 178 | `/api/lxmsmqjlwucg/odata/Samples`, but it's far from complete. 179 | Also, when importing into Excel, it rounds up time to seconds (instead of milliseconds). 180 | 181 | ## Partitions 182 | 183 | You can include `partition` numeric field in samples. All samples 184 | in a batch have to have the same partition. You can then specify 185 | `?partition=...` in query. Partition defaults to `0`, both when 186 | posting and querying. 187 | 188 | 189 | ## Quotas and throttling 190 | 191 | * 50MB per-stream limit 192 | * 32 fields per stream max 193 | * At most 1 post per stream per minute (bursts of up to 60 every hour acceptable) 194 | * Similarly, for queries 195 | * At most 1 api request from an IP per 2 seconds 196 | 197 | ## Error responses 198 | 199 | All endpoints describe here return HTTP 200 on success. 200 | 201 | In case you exceed throttling limit they return HTTP 429. It applies to creating streams, 202 | posting data, and querying. In case you're just a few seconds over the throttling limit, 203 | the server will block for these few seconds and then return HTTP 200. 204 | 205 | If you exceed the 50MB quota, you'll get HTTP 412 when posting data. 206 | 207 | HTTP 400 signals various error with the input request. Inspect JSON response for 208 | exact error message. 209 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs" 5 | }, 6 | "files": [ 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "babel-cli": "^6.7.5", 5 | "babel-plugin-transform-es2015-classes": "^6.6.0", 6 | "babel-plugin-transform-es2015-destructuring": "^6.6.0", 7 | "babel-plugin-transform-es2015-parameters": "^6.6.0", 8 | "babel-plugin-transform-es2015-object-super": "^6.6.5", 9 | "babel-plugin-transform-es2015-spread": "^6.6.5", 10 | "babel-preset-es2015": "^6.6.0", 11 | "tsd": "^0.5.7", 12 | "typescript": "^1.8.10" 13 | }, 14 | "dependencies": { 15 | "azure-storage": "^0.6.0", 16 | "azure-table-node": "^1.4.1", 17 | "bluebird": "^2.10.1", 18 | "jwt-simple": "^0.3.1", 19 | "loggly": "^1.0.8", 20 | "marked": "^0.3.5", 21 | "nodemailer": "^1.5.0", 22 | "nunjucks": "^2.1.0", 23 | "raygun": "^0.7.1", 24 | "redis": "^2.5.3", 25 | "reflect-metadata": "^0.1.2", 26 | "restify": "^4.0.3", 27 | "sendgrid": "^1.9.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /scripts/asynclint.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var fs = require('fs'); 3 | var numerr = 0; 4 | process.argv.slice(2).forEach(fn => { 5 | let lineNo = 0; 6 | fs.readFileSync(fn, "utf8").split(/\r?\n/).forEach(ln => { 7 | lineNo++; 8 | if (/^\s*(export\s+)?(async\s+)?function\s+/.test(ln)) return; 9 | if (/^\s*(public|private|static)\s+/.test(ln)) return; 10 | for (var i = 0; i < 5; ++i) 11 | ln = ln.replace(/(return|await|\/\* async \*\/)\s+(.*?)?\w+Async\(/, " [...snip...] ") 12 | if (/Async\(/.test(ln)) { 13 | console.log(`${fn}:${lineNo}: ${ln}`) 14 | numerr++ 15 | } 16 | }) 17 | }) 18 | 19 | if (numerr) process.exit(1) 20 | 21 | // vim: ts=4 sw=4 22 | -------------------------------------------------------------------------------- /scripts/mkaccounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | c="" 4 | echo -n > envadd.json 5 | 6 | pref=myservice 7 | 8 | function addone() { 9 | name=$pref$1 10 | v0=$3 11 | v1=$2 12 | azure storage account create --type RAGRS --location "East US" $name 13 | key=`azure storage account keys list $name --json | grep primaryKey | sed -e 's/.*: "//; s/",//g'` 14 | echo "\"$v0\": \"$name\"," >> envadd.json 15 | echo "\"$v1\": \"$key\"," >> envadd.json 16 | cat envadd.json 17 | echo "\"$name\": \"$key\"," >> accounts.json 18 | } 19 | 20 | 21 | addone "" AZURE_STORAGE_ACCESS_KEY AZURE_STORAGE_ACCOUNT & 22 | addone backup BACKUP_KEY BACKUP_ACCOUNT & 23 | exit 24 | addone ws0 WORKSPACE_BLOB_KEY0 WORKSPACE_BLOB_ACCOUNT0 & 25 | addone ws1 WORKSPACE_BLOB_KEY1 WORKSPACE_BLOB_ACCOUNT1 & 26 | addone ws2 WORKSPACE_BLOB_KEY2 WORKSPACE_BLOB_ACCOUNT2 & 27 | addone ws3 WORKSPACE_BLOB_KEY3 WORKSPACE_BLOB_ACCOUNT3 & 28 | addone wstab WORKSPACE_KEY WORKSPACE_ACCOUNT & 29 | addone hist WORKSPACE_HIST_KEY WORKSPACE_HIST_ACCOUNT & 30 | addone not NOTIFICATIONS_KEY NOTIFICATIONS_ACCOUNT & 31 | addone audit AUDIT_BLOB_KEY AUDIT_BLOB_ACCOUNT & 32 | addone streams STREAMS_KEY STREAMS_ACCOUNT & 33 | -------------------------------------------------------------------------------- /shell/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | node ../node_modules/typescript/bin/tsc 3 | -------------------------------------------------------------------------------- /shell/azure/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SET THIS=%~dp0 4 | 5 | cd .. 6 | node ../node_modules/typescript/bin/tsc 7 | cd azure 8 | 9 | "C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.9\bin\cspack.exe" %THIS%\tdshell.csdef /out:%THIS%\..\..\built\tdshell.cspkg /roleFiles:ShellRole;%THIS%\files.txt 10 | if %ERRORLEVEL% NEQ 0 ( 11 | echo Error building bootstrap.cspkg. Make sure cspack.exe from Windows Azure SDK is on the PATH. 12 | exit /b -1 13 | ) 14 | 15 | exit /b 0 16 | -------------------------------------------------------------------------------- /shell/azure/files.txt: -------------------------------------------------------------------------------- 1 | setup_worker.cmd;setup_worker.cmd 2 | start_worker.cmd;start_worker.cmd 3 | showlog.js;showlog.js 4 | ../../built/shell.js;shell.js 5 | -------------------------------------------------------------------------------- /shell/azure/setup_worker.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo %DATE% %TIME% Entering setup_worker.cmd 4 | 5 | SET NODE_URL=https://nodejs.org/dist/v0.12.0/node-v0.12.0-x86.msi 6 | 7 | SET THIS=%~dp0 8 | 9 | echo %DATE% %TIME% Granting permissions for all users to the deployment directory... 10 | icacls %THIS% /grant "Users":(OI)(CI)F 11 | if %ERRORLEVEL% NEQ 0 ( 12 | echo %DATE% %TIME% ERROR Granting permission 13 | exit /b -9 14 | ) 15 | echo %DATE% %TIME% Permissions granted 16 | 17 | if "%ZIP_URL%"=="" goto download_node 18 | 19 | echo %DATE% %TIME% Downloading zip... 20 | powershell -nologo -noprofile -c "Invoke-WebRequest %ZIP_URL% -OutFile %THIS%\pkg.zip" 21 | echo %DATE% %TIME% Unzipping... 22 | powershell -nologo -noprofile -c "& { Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('pkg.zip', '.'); }" 23 | echo %DATE% %TIME% Done unzipping. 24 | 25 | :download_node 26 | 27 | if exist %THIS%\node.msi goto install_node 28 | 29 | echo %DATE% %TIME% Downloading node.js... 30 | powershell -nologo -noprofile -c "Invoke-WebRequest %NODE_URL% -OutFile %THIS%\node.msi" 31 | 32 | :install_node 33 | 34 | echo %DATE% %TIME% Installing node.js... 35 | msiexec /i %THIS%\node.msi /q 36 | echo %ERRORLEVEL% 37 | if %ERRORLEVEL% NEQ 0 if %ERRORLEVEL% NEQ 1603 ( 38 | echo %DATE% %TIME% ERROR installing node.js %ERRORLEVEL% 39 | exit /b -2 40 | ) 41 | rem echo %DATE% %TIME% Node.js installed 42 | 43 | 44 | :end 45 | 46 | echo %DATE% %TIME% Exiting setup_worker.cmd (success) 47 | 48 | exit /b 0 49 | -------------------------------------------------------------------------------- /shell/azure/showlog.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fs = require('fs'); 3 | var port = process.env.HTTP_PORT || 1337; 4 | http.createServer(function (req, res) { 5 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 6 | if (req.url == "/log.txt") { 7 | res.end(fs.readFileSync("log.txt", "utf8")); 8 | } else { 9 | res.end('Hello World\n' + process.version + "\n" + JSON.stringify(process.env, null, 2)); 10 | } 11 | }).listen(port); 12 | -------------------------------------------------------------------------------- /shell/azure/start_worker.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SET THIS=%~dp0 4 | 5 | echo %DATE% %TIME% Entering start_worker.cmd 6 | 7 | "%programfiles(x86)%\nodejs\"node.exe shell.js 8 | echo %DATE% %TIME% shell.js runtime terminated with code %ERRORLEVEL% 9 | 10 | rem "%programfiles(x86)%\nodejs\"node.exe showlog.js 11 | rem echo %DATE% %TIME% showlog.js runtime terminated with code %ERRORLEVEL% 12 | 13 | echo %DATE% %TIME% Exiting start_worker.cmd 14 | -------------------------------------------------------------------------------- /shell/azure/tdshell.csdef: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /shell/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": false, 5 | "outDir": "../built", 6 | "newLine": "LF", 7 | "module": "commonjs", 8 | "sourceMap": false 9 | }, 10 | "exclude": [ 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | $(MAKE) -C .. 3 | -------------------------------------------------------------------------------- /src/cron.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | import * as restify from './restify'; 9 | import * as redis from './redis'; 10 | 11 | var logger: td.AppLogger; 12 | var redisClient: redis.Client; 13 | var jobs: Job[] = []; 14 | 15 | export class Job { 16 | constructor(public id: string, public everyMinutes: number, public callbackAsync: () => Promise) 17 | { 18 | } 19 | 20 | public scheduleRestart() 21 | { 22 | /* async */ redisClient.delAsync("cronlock:" + this.id) 23 | } 24 | } 25 | 26 | var lastCheck = 0; 27 | var lastPoke = 0; 28 | var jobRunning: Job; 29 | 30 | export function seemsAlive():boolean 31 | { 32 | if (restify.server().inShutdownMode) 33 | return false; 34 | return (Date.now() - lastPoke) < 20000; 35 | } 36 | 37 | export function poke() 38 | { 39 | if (restify.server().inShutdownMode) { 40 | logger.warning("got poke in shutdown mode") 41 | return 42 | } 43 | 44 | let now = Date.now(); 45 | lastPoke = now; 46 | if (!lastCheck) lastCheck = now; 47 | let delta = now - lastCheck 48 | if (delta < td.randomRange(20000, 60000)) 49 | return; 50 | 51 | lastCheck = now 52 | 53 | if (jobRunning) { 54 | logger.warning("job still running: " + jobRunning.id + "; skipping") 55 | return 56 | } 57 | 58 | setTimeout(runJobsAsync, 10); 59 | } 60 | 61 | export async function initAsync(): Promise { 62 | if (logger != null) return; 63 | logger = td.createLogger("cron"); 64 | logger.info("initialized"); 65 | redisClient = await redis.createClientAsync(); 66 | } 67 | 68 | async function runJobsAsync() { 69 | let jobsNow = jobs.slice(0) 70 | let start = Date.now(); 71 | td.permute(jobsNow) 72 | for (let j of jobsNow) { 73 | jobRunning = j; 74 | let runtime = Date.now() - start 75 | if (runtime > 30000) 76 | break 77 | logger.debug(`check job ${j.id}; loop @ ${runtime}ms`) 78 | let res = await redisClient.sendCommandAsync("set", ["cronlock:" + j.id, "working", "NX", "EX", j.everyMinutes * 60]) 79 | if (td.toString(res) != "OK") continue; 80 | logger.info("starting cronjob " + j.id) 81 | await j.callbackAsync() 82 | logger.info("finished cronjob " + j.id) 83 | } 84 | jobRunning = null; 85 | } 86 | 87 | export function registerJob(j:Job) 88 | { 89 | assert(jobs.filter(x => x.id == j.id).length == 0) 90 | jobs.push(j); 91 | } -------------------------------------------------------------------------------- /src/crowdin.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | import * as restify from './restify'; 9 | 10 | var logger: td.AppLogger; 11 | var apiRoot = "https://api.crowdin.com/api/project/touchdevelop/" 12 | var suff = "" 13 | 14 | export var enabled = false; 15 | 16 | export function init(): Promise { 17 | if (logger != null) return; 18 | enabled = true; 19 | logger = td.createLogger("crowdin"); 20 | suff = "?key=" + td.serverSetting("CROWDIN_KEY"); 21 | let prj = td.serverSetting("CROWDIN_PROJECT", true) 22 | if (prj) { 23 | apiRoot = "https://api.crowdin.com/api/project/" + prj + "/" 24 | } 25 | logger.info("initialized"); 26 | } 27 | 28 | export async function multipartPostAsync(uri: string, data: any, filename: string = null, filecontents: string = null) { 29 | // tried and failed to use request module... 30 | 31 | var boundry = "--------------------------0461489f461126c5" 32 | var form = "" 33 | 34 | function add(name: string, val: string) { 35 | form += boundry + "\r\n" 36 | form += "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" 37 | form += val + "\r\n" 38 | } 39 | 40 | function addF(name: string, val: string) { 41 | form += boundry + "\r\n" 42 | form += "Content-Disposition: form-data; name=\"files[" + name + "]\"; filename=\"blah.json\"\r\n" 43 | form += "\r\n" 44 | form += val + "\r\n" 45 | } 46 | 47 | Object.keys(data).forEach(k => add(k, data[k])) 48 | if (filename) 49 | addF(filename, filecontents) 50 | 51 | form += boundry + "--\r\n" 52 | 53 | let req = td.createRequest(uri) 54 | req.setMethod("POST"); 55 | req.setContent(form); 56 | req.setContentType("multipart/form-data; boundary=" + boundry.slice(2)) 57 | 58 | let resp = await req.sendAsync() 59 | return resp; 60 | } 61 | 62 | export async function uploadTranslationAsync(filename: string, jsondata: {}) { 63 | let cnt = 0 64 | 65 | function incr() { 66 | if (cnt++ > 10) { 67 | throw new Error("Too many API calls for " + filename); 68 | } 69 | } 70 | 71 | async function createDirAsync(name: string) { 72 | await createDir0Async(name); 73 | } 74 | 75 | async function createDir0Async(name: string) { 76 | logger.info(`create directory ${name}`) 77 | incr(); 78 | let resp = await multipartPostAsync(apiRoot + "add-directory" + suff, { json: "", name: name }); 79 | if (resp.statusCode() == 200) 80 | return; 81 | 82 | let data: any = resp.contentAsJson() || { error: {} } 83 | 84 | if (resp.statusCode() == 404 && data.error.code == 17) { 85 | logger.info(`parent directory missing for ${name}`) 86 | var par = name.replace(/\/[^\/]+$/, "") 87 | if (par != name) { 88 | await createDirAsync(par); 89 | await createDirAsync(name); // retry 90 | return 91 | } 92 | } 93 | 94 | throw new Error(`cannot create dir ${name}: ${resp.toString() } ${JSON.stringify(data) }`) 95 | } 96 | 97 | async function startAsync() { 98 | await uploadAsync("update-file", { update_option: "update_as_unapproved" }) 99 | } 100 | 101 | async function uploadAsync(op: string, opts: any) { 102 | opts["type"] = "auto"; 103 | opts["json"] = ""; 104 | incr(); 105 | let resp = await multipartPostAsync(apiRoot + op + suff, opts, filename, JSON.stringify(jsondata)) 106 | await handleResponseAsync(resp); 107 | } 108 | 109 | async function handleResponseAsync(resp: td.WebResponse) { 110 | let code = resp.statusCode(); 111 | let data: any = resp.contentAsJson() || { error: {} } 112 | 113 | logger.debug(`response ${resp.statusCode() } ${JSON.stringify(data) }`) 114 | 115 | if (code == 404 && data.error.code == 8) { 116 | logger.info(`create new translation file: ${filename}`) 117 | await uploadAsync("add-file", {}) 118 | } 119 | else if (code == 404 && data.error.code == 17) { 120 | await createDirAsync(filename.replace(/\/[^\/]+$/, "")); 121 | await startAsync(); 122 | } else if (code == 200) { 123 | return 124 | } else { 125 | throw new Error(`Error, upload translation: ${filename}, ${resp}, ${JSON.stringify(data) }`) 126 | } 127 | } 128 | 129 | await startAsync(); 130 | } 131 | 132 | var inlineTags: td.SMap = { 133 | b: 1, 134 | strong: 1, 135 | em: 1, 136 | } 137 | 138 | export function translate(html: string, locale: td.SMap) { 139 | let missing = {} 140 | 141 | function translateOne(toTranslate: string): string { 142 | let spm = (/^(\s*)([^]*?)(\s*)$/.exec(toTranslate) || []); 143 | let text = spm[2].replace(/\s+/g, " "); 144 | if (text == "" || /^(@\w+@|\{[^\{\}]+\}|[^a-zA-Z]*|( )+)$/.test(text)) 145 | return null; 146 | if (locale.hasOwnProperty(text)) 147 | text = locale[text]; 148 | else 149 | missing[text] = ""; 150 | return spm[1] + text + spm[3]; 151 | } 152 | 153 | html = html.replace(/<([\/\w]+)([^<>]*)>/g, (full: string, tagname: string, args: string) => { 154 | let key = tagname.replace(/^\//, "").toLowerCase(); 155 | if (inlineTags[key] === 1) 156 | return "&llt;" + tagname + args + "&ggt;"; 157 | return full; 158 | }); 159 | 160 | function ungt(s: string) { 161 | return s.replace(/&llt;/g, "<").replace(/&ggt;/g, ">"); 162 | } 163 | 164 | html = "" + html; 165 | html = html.replace(/(<([\/\w]+)([^<>]*)>)([^<>]+)/g, (full: string, fullTag: string, tagname: string, args: string, str: string) => { 166 | if (tagname == "script" || tagname == "style") 167 | return ungt(full) 168 | 169 | let tr = translateOne(ungt(str)); 170 | if (tr == null) 171 | return ungt(full); 172 | return fullTag + tr; 173 | }); 174 | 175 | html = html.replace(/(<[^<>]*)(placeholder|alt|title)="([^"]+)"/g, (full: string, pref: string, attr: string, text: string) => { 176 | let tr = translateOne(text); 177 | if (tr == null) return full; 178 | return pref + attr + '="' + text.replace(/"/g, "''") + '"'; 179 | }); 180 | 181 | html = html.replace(/^/g, ""); 182 | return { 183 | text: html, 184 | missing: missing 185 | } 186 | } 187 | 188 | export async function downloadTranslationAsync(filename: string, lang: string) { 189 | let req = td.createRequest(apiRoot + "export-file" + suff + 190 | "&file=" + encodeURIComponent(filename) + 191 | "&language=" + encodeURIComponent(lang)) 192 | let resp = await req.sendAsync() 193 | let data = resp.contentAsJson() || {} 194 | let res: td.SMap = {} 195 | for (let k of Object.keys(data)) 196 | if (data[k] != "") 197 | res[k] = data[k]; 198 | return res; 199 | } 200 | -------------------------------------------------------------------------------- /src/kraken.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | 13 | var logger: td.AppLogger; 14 | var apiSecret: string = ""; 15 | var apiKey: string = ""; 16 | 17 | 18 | export interface IOptimizeOptions { 19 | width?: number; 20 | height?: number; 21 | resizeStrategy?: string; 22 | lossy?: boolean; 23 | quality?: number; 24 | webp?: boolean; 25 | callbackUrl?: string; 26 | retries?: number; 27 | } 28 | 29 | 30 | /** 31 | * Initializes the library. If empty, the ``api key`` and ``api secret`` values are read from the server settings ``KRAKEN_API_KEY`` and ``KRAKEN_API_SECRET``. The library is also setup to store the files in an Azure Blob storage container for which you can provide the information through ``AZURE_STORAGE_ACCOUNT`` and ``AZURE_STORAGE_ACCESS_KEY``. 32 | */ 33 | export function init(apiKey_: string, apiSecret_: string) : void 34 | { 35 | assert(logger == null, "double initialization"); 36 | logger = td.createLogger("kraken"); 37 | apiKey = apiKey_ || td.serverSetting("KRAKEN_API_KEY", false); 38 | apiSecret = apiSecret_ || td.serverSetting("KRAKEN_API_SECRET", false); 39 | } 40 | 41 | async function sendRequestAsync(url: string, payload: JsonBuilder, retry: number) : Promise 42 | { 43 | let resp: JsonObject; 44 | resp = (null); 45 | let request = td.createRequest(url); 46 | request.setMethod("POST"); 47 | request.setContentAsJson(td.clone(payload)); 48 | let response = await request.sendAsync(); 49 | let res = response.contentAsJson(); 50 | logger.debug("optimize status: " + response.statusCode()); 51 | if (res != null) { 52 | logger.debug("optimized res: " + JSON.stringify(res, null, 2)); 53 | } 54 | if (response.statusCode() == 415 && retry > 0) { 55 | logger.debug("retrying... " + retry); 56 | resp = await sendRequestAsync(url, payload, retry - 1); 57 | } 58 | else if (response.statusCode() == 200) { 59 | resp = res; 60 | logger.debug("optimized id: " + resp); 61 | } 62 | else { 63 | logger.error("failed to krak picture - " + response.statusCode()); 64 | } 65 | return resp; 66 | } 67 | 68 | function prepareRequest(pictureUrl: string, options: IOptimizeOptions) : JsonBuilder 69 | { 70 | let payload: JsonBuilder; 71 | assert(logger != null, "did you forget to call init?"); 72 | assert(pictureUrl != "", "missing picture url"); 73 | payload = { "auth": {} }; 74 | payload["url"] = pictureUrl; 75 | if (!options.retries) { 76 | options.retries = 2; 77 | } 78 | if (!options.callbackUrl) { 79 | payload["wait"] = true; 80 | } 81 | else { 82 | payload["wait"] = false; 83 | payload["callback_url"] = options.callbackUrl; 84 | } 85 | if (options.lossy) { 86 | payload["lossy"] = options.lossy; 87 | if (options.quality) { 88 | payload["quality"] = options.quality; 89 | } 90 | } 91 | if (options.webp) { 92 | payload["webp"] = true; 93 | } 94 | let resizeStrategy = options.resizeStrategy || ""; 95 | if (resizeStrategy != "" && resizeStrategy != "none") { 96 | let resize = {}; 97 | resize["strategy"] = resizeStrategy; 98 | if (resizeStrategy == "square") { 99 | resize["size"] = options.width || options.height || 100; 100 | } else { 101 | if (options.width) { 102 | resize["width"] = options.width; 103 | } 104 | if (options.height) { 105 | resize["height"] = options.height; 106 | } 107 | } 108 | payload["resize"] = resize; 109 | } 110 | logger.debug("optimize: " + JSON.stringify(payload, null, 2)); 111 | let auth = payload["auth"]; 112 | auth["api_key"] = apiKey; 113 | auth["api_secret"] = apiSecret; 114 | return payload; 115 | } 116 | 117 | /** 118 | * Schedules a file to be optimized by the Kraken optimization service. 119 | */ 120 | export async function optimizePictureUrlAsync(pictureUrl: string, options: IOptimizeOptions = {}) : Promise 121 | { 122 | let idOrUrl: string; 123 | let payload = prepareRequest(pictureUrl, options); 124 | let url = "https://api.kraken.io/v1/url"; 125 | let resp = await sendRequestAsync(url, payload, options.retries); 126 | idOrUrl = processResponse(options, resp); 127 | return idOrUrl; 128 | } 129 | 130 | function processResponse(options: IOptimizeOptions, resp: JsonObject) : string 131 | { 132 | let idOrUrl: string; 133 | if (!options.callbackUrl) { 134 | idOrUrl = resp["kraked_url"]; 135 | } 136 | else { 137 | idOrUrl = null; 138 | } 139 | return idOrUrl; 140 | } 141 | 142 | 143 | -------------------------------------------------------------------------------- /src/loggly.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | var logger: td.AppLogger; 13 | var logglyLevel = 0; 14 | 15 | 16 | export interface IOptions { 17 | token?: string; 18 | subdomain?: string; 19 | globalTags?: string; 20 | uploadInterval?: number; 21 | minLevel?: number; 22 | } 23 | 24 | var loggly = require('loggly'); 25 | 26 | 27 | /** 28 | * Initializes loggly with the account information. If ``token`` is not provided, the ``LOGGLY_TOKEN`` server setting is used. If ``sub domain`` is not provided, ``LOGGLY_SUB_DOMAIN`` is provided. 29 | */ 30 | export async function initAsync(options_: IOptions = {}) : Promise 31 | { 32 | assert(logger == null, "multiple initialization"); 33 | if (!options_.token) { 34 | options_.token = td.serverSetting("LOGGLY_TOKEN", false); 35 | } 36 | if (!options_.subdomain) { 37 | options_.subdomain = td.serverSetting("LOGGLY_SUB_DOMAIN", false); 38 | } 39 | if (!options_.uploadInterval) { 40 | options_.uploadInterval = 2; 41 | } 42 | if (!options_.minLevel) { 43 | options_.minLevel = 7; 44 | } 45 | setMinLevel(options_.minLevel); 46 | logger = td.createLogger("loggly"); 47 | let allTags = options_.globalTags.split(";"); 48 | allTags.push("TouchDevelop"); 49 | let globalTags = allTags.join(";"); 50 | initProxy(options_.token, options_.subdomain, globalTags, options_.uploadInterval); 51 | } 52 | 53 | async function exampleAsync() : Promise 54 | { 55 | // Sends logging messages to [loggly.com](http://www.loggly.com). 56 | // ### configuration 57 | // Set the ``LOGGLY_TOKEN`` and ``LOGGLY_SUB_DOMAIN`` server setting to your token and subdomain. 58 | await initAsync({ 59 | globalTags: "myapp" 60 | }); 61 | // That's it! 62 | } 63 | 64 | function initProxy(token: string, subDomain: string, globalTags: string, seconds: number) : void 65 | { 66 | let logglyClient = loggly.createClient({ 67 | token: token, 68 | subdomain: subDomain, 69 | tags: globalTags.split(';'), 70 | json: true, 71 | useTagHeader: false, 72 | }); 73 | let logglyLogs = []; 74 | td.App.addTransport({ 75 | log : function(level, cat, msg, meta) { 76 | if (level <= logglyLevel) { 77 | logglyLogs.push({ 78 | level: level, 79 | category: cat, 80 | message: msg, 81 | meta: meta }); 82 | } 83 | }, 84 | logException: function(err,meta) {} 85 | }); 86 | // batching 87 | setInterval(function() { 88 | if (logglyLogs.length > 0) { 89 | var lgs = logglyLogs; 90 | logglyLogs = []; 91 | var retry = 10; 92 | //console.log("sending to loggly, #msgs: ", lgs.length) 93 | var send = function() { 94 | logglyClient.log(lgs, function (err, result) { 95 | if(err) { 96 | if (retry-- > 0 && (err.code == "ETIMEDOUT" || err.code == "ECONNRESET")) 97 | setTimeout(send, 2000); 98 | else 99 | td.App.logException(err); 100 | } 101 | }); 102 | } 103 | send() 104 | } 105 | }, seconds * 1000); 106 | } 107 | 108 | /** 109 | * Sets the minimum level of messages to be logged (``debug=7``, ``info=6``, ``warning=4``, ``error=3``) 110 | * {hints:level:6,7,3,4} 111 | */ 112 | export function setMinLevel(level: number) : void 113 | { 114 | level = Math.floor(level); 115 | logglyLevel = level 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/microsoft-translator.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | var accessToken: string; 12 | var clientId: string = ""; 13 | var clientSecret: string = ""; 14 | var logger: td.AppLogger; 15 | 16 | 17 | export interface ITokenReq { 18 | grant_type?: string; 19 | client_id?: string; 20 | client_secret?: string; 21 | scope?: string; 22 | } 23 | 24 | 25 | async function createRequestAsync(url: string, method: string, authorize: boolean) : Promise 26 | { 27 | let request:td.WebRequest; 28 | request = td.createRequest(url); 29 | if (authorize) { 30 | await authenticateAsync(); 31 | setAuthorization(request); 32 | } 33 | request.setMethod(method); 34 | return request; 35 | } 36 | 37 | function setAuthorization(request:td.WebRequest) : void 38 | { 39 | request.setHeader("Authorization", "Bearer " + accessToken); 40 | } 41 | 42 | export async function authenticateAsync() : Promise 43 | { 44 | let authenticated: boolean; 45 | if (accessToken == null) { 46 | logger.debug("refreshing token..."); 47 | let url = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13"; 48 | let tokenReq:ITokenReq = {}; 49 | tokenReq.grant_type = "client_credentials"; 50 | tokenReq.scope = "http://api.microsofttranslator.com"; 51 | tokenReq.client_secret = clientSecret; 52 | tokenReq.client_id = clientId; 53 | let request = await createRequestAsync(url, "POST", false); 54 | let content = toQueryString(tokenReq); 55 | request.setContent(content); 56 | request.setHeader("Content-type", "application/x-www-form-urlencoded"); 57 | let response = await request.sendAsync(); 58 | if (response.statusCode() == 200) { 59 | let js = response.contentAsJson(); 60 | accessToken = js["access_token"] || null; 61 | if (accessToken != null) { 62 | } 63 | else { 64 | logger.error("failed to parse access token"); 65 | } 66 | } 67 | else { 68 | logger.error("failed getting access token: " + response.statusCode()); 69 | logger.debug(response.content()); 70 | } 71 | } 72 | authenticated = accessToken != null; 73 | return authenticated; 74 | } 75 | 76 | function toQueryString(params: JsonObject) : string 77 | { 78 | let query: string; 79 | query = ""; 80 | for (let s2 of Object.keys(params)) { 81 | if (query != "") { 82 | query = query + "&"; 83 | } 84 | query = query + encodeURIComponent(s2) + "=" + encodeURIComponent(params[s2]); 85 | } 86 | return query; 87 | } 88 | 89 | async function sendRequestAsync(request:td.WebRequest) : Promise 90 | { 91 | let response:td.WebResponse; 92 | response = await request.sendAsync(); 93 | logger.debug("status: " + response.statusCode()); 94 | if (response.statusCode() == 400) { 95 | logger.debug("token expired"); 96 | accessToken = null; 97 | let authenticated = await authenticateAsync(); 98 | if (authenticated) { 99 | setAuthorization(request); 100 | response = await request.sendAsync(); 101 | logger.debug("status (refresh): " + response.statusCode()); 102 | } 103 | } 104 | return response; 105 | } 106 | 107 | async function testAuthenticateAsync() : Promise 108 | { 109 | accessToken = null; 110 | await initAsync("TouchDevelopTranslator", ""); 111 | await authenticateAsync(); 112 | assert(!!accessToken, ""); 113 | } 114 | 115 | /** 116 | * Translates the given text to the given language 117 | */ 118 | export async function translateAsync(text: string, from: string, to: string, html: boolean) : Promise 119 | { 120 | let translated: string; 121 | checkCredentials(); 122 | assert(to != "", "missing target language"); 123 | let type = "text/plain"; 124 | if (html) { 125 | type = "text/html"; 126 | } 127 | let url = "http://api.microsofttranslator.com/v2/Http.svc/Translate?text=" + encodeURIComponent(text) + "&to=" + to + "&contentType=" + type; 128 | if (from != "") { 129 | url = url + "&from=" + from; 130 | } 131 | let request = await createRequestAsync(url, "get", true); 132 | let response = await sendRequestAsync(request); 133 | // .... 134 | if (response.statusCode() == 200) { 135 | let xml = response.content(); 136 | xml = xml.replace(/<[^>]*>/g, ""); 137 | xml = td.replaceAll(xml, "<", "<"); 138 | xml = td.replaceAll(xml, ">", ">"); 139 | xml = td.replaceAll(xml, "&", "&"); 140 | translated = xml; 141 | } 142 | else { 143 | translated = null; 144 | } 145 | return translated; 146 | } 147 | 148 | /** 149 | * Initializes the credentials needed to call the Microsoft Translator APIs. If not provided, the ``MICROSOFT_TRANSLATOR_CLIENT_ID`` and ``MICROSOFT_TRANSLATOR_CLIENT_SECRET`` server setting are used. 150 | */ 151 | export async function initAsync(clientId_ = "", clientSecret_ = ""): Promise { 152 | clientId = clientId_ || td.serverSetting("MICROSOFT_TRANSLATOR_CLIENT_ID", false); 153 | clientSecret = clientSecret_ || td.serverSetting("MICROSOFT_TRANSLATOR_CLIENT_SECRET", false); 154 | logger = td.createLogger("translator"); 155 | logger.debug("client id: " + clientId); 156 | } 157 | 158 | function checkCredentials() : void 159 | { 160 | assert(clientId != "" && clientSecret != "", "missing credentials, did you call `init`?"); 161 | } 162 | 163 | async function exampleAsync() : Promise 164 | { 165 | // This library allows to send translation requests to the Microsoft Translator services. 166 | // ## configuration 167 | // The ``init`` action takes the ``MICROSOFT_TRANSLATOR_CLIENT_ID`` and ``MICROSOFT_TRANSLATOR_CLIENT_SECRET`` server setting from the environment. 168 | await initAsync("", ""); 169 | // ### translations 170 | // To translate a string, provide the input language (leave empty to let translator guess), the target language and whether the text is html. 171 | let translated = await translateAsync("hello", "en", "fr", false); 172 | } 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/nodemailer.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | var logger: td.AppLogger; 13 | var nodemailer:any; 14 | 15 | export interface ISendOptions { 16 | html?: string; 17 | bcc?: string; 18 | cc?: string; 19 | replyto?: string; 20 | } 21 | 22 | 23 | async function exampleAsync() : Promise 24 | { 25 | // This library provides a simple access to sending email directly from the current machine. This may or may not work, and it may be considered spam. 26 | // {imports} 27 | // ### configuration 28 | // Call the `init` action in your web service `_init` action. 29 | await initAsync(); 30 | // ### simple usage 31 | // Use the ``send`` action to send a simple text email. 32 | await sendAsync("to@example.com", "from@example.com", "Hi!", "This is my first email through SendGrid."); 33 | } 34 | 35 | 36 | /** 37 | * Initializes the nodemailer library with the direct transport. 38 | */ 39 | export async function initAsync() : Promise 40 | { 41 | logger = td.createLogger("nodemailer"); 42 | nodemailer = require('nodemailer').createTransport(); 43 | } 44 | 45 | /** 46 | * Sends a text email. 47 | */ 48 | export async function sendAsync(to: string, from: string, subject: string, text: string, options_: ISendOptions = {}) : Promise 49 | { 50 | assert(!!to, "missing to"); 51 | assert(!!from, "missing from"); 52 | assert(!!subject, "missing subject"); 53 | logger.debug("sending email"); 54 | 55 | await new Promise((resume, reject) => { 56 | var opts = options_ 57 | var payload:any = { 58 | to : to, 59 | from : from, 60 | subject: subject, 61 | text: text, 62 | html: opts.html, 63 | replyTo: opts.replyto 64 | }; 65 | if (opts.bcc) payload.bcc = opts.bcc.split(';'); 66 | if (opts.cc) payload.cc = opts.cc.split(';'); 67 | nodemailer.sendMail(payload, (err, json) => { 68 | if (err) reject(err); 69 | else resume(); 70 | }); 71 | }); 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/parallel.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | 7 | export class Queue 8 | extends td.JsonRecord 9 | { 10 | public numRunning: number = 0; 11 | public maxRunning: number = 0; 12 | public toRun: td.Action[] = []; 13 | 14 | /** 15 | * Add a new task to the queue. The task will run when queue has spare capacity. 16 | */ 17 | public schedule(task:td.Action) : void 18 | { 19 | this.toRun.push(task); 20 | this.pokeRunQueue(); 21 | } 22 | 23 | private pokeRunQueue() : void 24 | { 25 | let q: Queue = this; 26 | while (q.toRun.length > 0 && q.numRunning < q.maxRunning) { 27 | let action2 = q.toRun[0]; 28 | q.toRun.splice(0, 1); 29 | q.numRunning += 1; 30 | process.nextTick(() => /* async */ q.runAndPokeAsync(action2)); 31 | } 32 | } 33 | 34 | private async runAndPokeAsync(action:td.Action) : Promise< void > 35 | { 36 | let q: Queue = this; 37 | await action(); 38 | q.numRunning -= 1; 39 | q.pokeRunQueue(); 40 | } 41 | 42 | /** 43 | * Waits until there are no more tasks running in the queue. 44 | */ 45 | public async waitForEmptyAsync() : Promise< void > 46 | { 47 | while (this.numRunning > 0) { 48 | await td.sleepAsync(0.1); 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Runs the ``action`` for the elements of a collection in parralel 55 | */ 56 | export async function forAsync(count: number, action:td.NumberAction, maxTasks = 0) : Promise< void > 57 | { 58 | if (maxTasks > 0) { 59 | let q = createQueue(maxTasks); 60 | for (let i = 0; i < count; i++) { 61 | let tmp = i; 62 | q.schedule(async () => { 63 | await action(tmp); 64 | }) 65 | } 66 | await q.waitForEmptyAsync(); 67 | } else { 68 | let coll = []; 69 | for (let i = 0; i < count; i++) { 70 | coll.push(action(i)); 71 | } 72 | for (let task2 of coll) { 73 | await task2; 74 | } 75 | } 76 | } 77 | 78 | async function exampleAsync() : Promise< void > 79 | { 80 | // A library to run operations in parallel 81 | // {hide} 82 | let entries = "a;b;c".split(";"); 83 | // {/hide} 84 | // ### for 85 | // The ``for`` action takes the number of element and an action to be called on each index. The action will be run `async` on each element. 86 | await forAsync(entries.length, async(x: number) => { 87 | let s = entries[x]; 88 | // do something with s 89 | }); 90 | // ### for batches 91 | // Similar to `code->for` but wait for groups of elements to finish processing. 92 | await forBatchedAsync(entries.length, 10, async(x1: number) => { 93 | let s1 = entries[x1]; 94 | // do something with s 95 | } 96 | , async() => { 97 | }); 98 | // ### for json 99 | // Similar to `code->for` but applies the action to the array element or field elements of JSON value. 100 | // {hide} 101 | let js = ({}); 102 | // {/hide} 103 | await forJsonAsync(js, async(jchild: td.JsonObject) => { 104 | // do something with ``child`` 105 | }); 106 | // ### queue 107 | // `queue` is used to run at most `N` async tasks at once. For example, the following code will initiate at most 5 downloads at once: 108 | let queue = createQueue(5); 109 | } 110 | 111 | /** 112 | * Runs the ``action`` for the elements of a collection in parralel in batches. Waits for each batch to finish before starting on the next one. The batch action gets executed after each batch. 113 | */ 114 | export async function forBatchedAsync(count: number, batchCount: number, itemAction:td.NumberAction, batchAction:td.Action) : Promise< void > 115 | { 116 | let coll = []; 117 | let i = 0; 118 | while (i < count) { 119 | let c = Math.min(batchCount, count - i); 120 | await forAsync(c, async(x: number) => { 121 | let j = i + x; 122 | await itemAction(j); 123 | }); 124 | await batchAction(); 125 | i = i + c; 126 | } 127 | } 128 | 129 | /** 130 | * Construct a new queue that can run up to `max running` tasks at a time. 131 | * {hints:max running:5} 132 | */ 133 | export function createQueue(maxRunning: number) : Queue 134 | { 135 | let queue = new Queue(); 136 | queue.maxRunning = maxRunning; 137 | return queue; 138 | } 139 | 140 | /** 141 | * Applies the ``action`` action to the array element or field values. 142 | */ 143 | export async function forJsonAsync(js: T[], action:td.Action1, maxTasks = 0) : Promise< void > 144 | { 145 | if (Array.isArray(js)) { 146 | await forAsync(js.length, async(x: number) => { 147 | let jsi = js[x]; 148 | await action(jsi); 149 | }, maxTasks); 150 | } else { 151 | let keys = Object.keys(js); 152 | await forAsync(keys.length, async(x1: number) => { 153 | let jsf = js[keys[x1]]; 154 | await action(jsf); 155 | }, maxTasks); 156 | } 157 | } 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/sendgrid.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | var logger: td.AppLogger; 12 | var sendgrid:any; 13 | 14 | 15 | export interface ISendOptions { 16 | html?: string; 17 | bcc?: string; 18 | cc?: string; 19 | replyto?: string; 20 | } 21 | 22 | 23 | async function exampleAsync() : Promise 24 | { 25 | // This library provides a simple access to [SendGrid](http://sendgrid.com/home). 26 | // {imports} 27 | // ### configuration 28 | // Call the `init` action in your web service `_init` action. You will need to configure the ``SENDGRID_API_USER`` and ``SENDGRID_API_KEY`` environment variables to point to your SendGrid username and api key. 29 | await initAsync("", ""); 30 | // ### simple usage 31 | // Use the ``send`` action to send a simple text email. 32 | await sendAsync("to@example.com", "from@example.com", "Hi!", "This is my first email through SendGrid."); 33 | } 34 | 35 | /** 36 | * Initializes the SendGrid library. If empty, reads the api user and api key from the ``SENDGRID_API_USER`` and ``SENDGRID_API_KEY`` server settings. 37 | */ 38 | export async function initAsync(apiUser: string = "", apiKey: string = "") : Promise 39 | { 40 | if (apiUser == "") { 41 | apiUser = td.serverSetting("SENDGRID_API_USER", false); 42 | } 43 | if (apiKey == "") { 44 | apiKey = td.serverSetting("SENDGRID_API_KEY", false); 45 | } 46 | logger = td.createLogger("sendgrid"); 47 | sendgrid = require('sendgrid')(apiUser, apiKey); 48 | } 49 | 50 | /** 51 | * Sends a text email. 52 | */ 53 | export async function sendAsync(to: string, from: string, subject: string, text: string, options: ISendOptions = {}) : Promise 54 | { 55 | assert(to != "", "missing to"); 56 | assert(from != "", "missing from"); 57 | assert(subject != "", "missing subject"); 58 | logger.debug("sending email"); 59 | await new Promise(resume => { 60 | var opts = options 61 | var payload:any = { 62 | to : to, 63 | from : from, 64 | subject: subject, 65 | text: text, 66 | html: opts.html, 67 | replyto: opts.replyto 68 | }; 69 | if (opts.bcc) payload.bcc = opts.bcc.split(';'); 70 | if (opts.cc) payload.cc = opts.cc.split(';'); 71 | sendgrid.send(payload, (err, json) => { 72 | if (err) throw new Error("sendgrid error: " + err) 73 | resume(); 74 | }); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /src/tdlite-comments.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | import * as indexedStore from "./indexed-store" 13 | import * as core from "./tdlite-core" 14 | import * as search from "./tdlite-search" 15 | import * as notifications from "./tdlite-notifications" 16 | 17 | var orEmpty = td.orEmpty; 18 | 19 | var logger = core.logger; 20 | var httpCode = core.httpCode; 21 | 22 | var comments: indexedStore.Store; 23 | 24 | export class PubComment 25 | extends core.PubOnPub 26 | { 27 | @td.json public text: string = ""; 28 | @td.json public nestinglevel: number = 0; 29 | @td.json public positivereviews: number = 0; 30 | @td.json public subscribers: number = 0; 31 | @td.json public comments: number = 0; 32 | @td.json public assignedtoid: string = ""; 33 | @td.json public resolved: string = ""; 34 | static createFromJson(o:JsonObject) { let r = new PubComment(); r.fromJson(o); return r; } 35 | } 36 | 37 | export async function initAsync() : Promise 38 | { 39 | comments = await indexedStore.createStoreAsync(core.pubsContainer, "comment"); 40 | core.registerPubKind({ 41 | store: comments, 42 | deleteWithAuthor: true, 43 | importOne: importCommentAsync, 44 | }) 45 | await core.setResolveAsync(comments, async (fetchResult: indexedStore.FetchResult, apiRequest: core.ApiRequest) => { 46 | await resolveCommentsAsync(fetchResult); 47 | } 48 | , { 49 | byUserid: true, 50 | byPublicationid: true 51 | }); 52 | core.addRoute("POST", "*pub", "comments", async (req: core.ApiRequest) => { 53 | await core.canPostAsync(req, "comment"); 54 | if (req.status == 200) { 55 | await postCommentAsync(req); 56 | } 57 | }); 58 | core.addRoute("GET", "*pub", "comments", async (req1: core.ApiRequest) => { 59 | if (req1.status == 200) { 60 | // optimize the no-comments case 61 | if (core.orZero(req1.rootPub["pub"]["comments"]) == 0) { 62 | req1.response = ({"continuation":"","items":[],"kind":"list"}); 63 | } 64 | else { 65 | await core.anyListAsync(comments, req1, "publicationid", req1.rootId); 66 | } 67 | } 68 | }, { override: true }); 69 | } 70 | 71 | async function resolveCommentsAsync(entities: indexedStore.FetchResult) : Promise 72 | { 73 | await core.addUsernameEtcAsync(entities); 74 | let coll = ([]); 75 | for (let jsb of entities.items) { 76 | let comment = PubComment.createFromJson(jsb["pub"]); 77 | coll.push(comment); 78 | } 79 | entities.items = td.arrayToJson(coll); 80 | } 81 | 82 | export async function getRootPubAsync(js: {}) 83 | { 84 | let max = 3; 85 | 86 | while (max-- > 0) { 87 | let cmt = PubComment.createFromJson(js["pub"]) 88 | js = await core.getPubAsync(cmt.publicationid, cmt.publicationkind); 89 | if (!js || cmt.publicationkind != "comment") 90 | return js; 91 | } 92 | 93 | return <{}>null; 94 | } 95 | 96 | async function postCommentAsync(req: core.ApiRequest) : Promise 97 | { 98 | let baseKind = req.rootPub["kind"]; 99 | if ( ! /^(comment|script|group|screenshot|channel)$/.test(baseKind)) { 100 | req.status = httpCode._412PreconditionFailed; 101 | } 102 | else { 103 | let comment = new PubComment(); 104 | comment.text = orEmpty(req.body["text"]); 105 | comment.userplatform = core.getUserPlatforms(req); 106 | comment.userid = req.userid; 107 | comment.time = await core.nowSecondsAsync(); 108 | comment.publicationid = req.rootId; 109 | comment.publicationkind = baseKind; 110 | if (baseKind == "comment") { 111 | comment.nestinglevel = req.rootPub["pub"]["nestinglevel"] + 1; 112 | comment.publicationname = req.rootPub["pub"]["publicationname"]; 113 | } 114 | else { 115 | comment.nestinglevel = 0; 116 | comment.publicationname = orEmpty(req.rootPub["pub"]["name"]); 117 | } 118 | let jsb = {}; 119 | jsb["pub"] = comment.toJson(); 120 | req.setCreatorInfo(jsb) 121 | await core.generateIdAsync(jsb, 10); 122 | await comments.insertAsync(jsb); 123 | await updateCommentCountersAsync(comment); 124 | await notifications.storeAsync(req, jsb, ""); 125 | await search.scanAndSearchAsync(jsb); 126 | // ### return comment back 127 | await core.returnOnePubAsync(comments, td.clone(jsb), req); 128 | } 129 | } 130 | 131 | async function importCommentAsync(req: core.ApiRequest, body: JsonObject) : Promise 132 | { 133 | let comment = new PubComment(); 134 | comment.fromJson(core.removeDerivedProperties(body)); 135 | 136 | let jsb = {}; 137 | jsb["pub"] = comment.toJson(); 138 | jsb["id"] = comment.id; 139 | await comments.insertAsync(jsb); 140 | await search.scanAndSearchAsync(jsb, { 141 | skipScan: true 142 | }); 143 | await updateCommentCountersAsync(comment); 144 | } 145 | 146 | /** 147 | * ### update comment count 148 | */ 149 | async function updateCommentCountersAsync(comment: PubComment) : Promise 150 | { 151 | await core.pubsContainer.updateAsync(comment.publicationid, async (entry: JsonBuilder) => { 152 | core.increment(entry, "comments", 1); 153 | }); 154 | } 155 | 156 | -------------------------------------------------------------------------------- /src/tdlite-docs.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | 'use strict'; 5 | 6 | import * as td from './td'; 7 | import * as assert from 'assert'; 8 | import * as marked from 'marked'; 9 | 10 | type JsonObject = td.JsonObject; 11 | type JsonBuilder = td.JsonBuilder; 12 | 13 | var expandInfo: td.Action1; 14 | 15 | require("./backendutils") 16 | declare var pxt: any; 17 | 18 | export interface BreadcrumbEntry { 19 | name: string; 20 | href: string; 21 | } 22 | 23 | export var renderMarkdown: (template: string, src: string, theme: {}, pubinfo?: {}, breadcrumb?: BreadcrumbEntry[]) => string = pxt.docs.renderMarkdown; 24 | //export var embedUrl: (rootUrl: string, id: string, height?: number)=> string = pxt.docs.embedUrl; 25 | 26 | export async function formatAsync(templ: string, pubdata: JsonBuilder): Promise { 27 | if (pubdata["time"] != null) { 28 | pubdata["timems"] = pubdata["time"] * 1000; 29 | pubdata["humantime"] = humanTime(new Date(pubdata["timems"])); 30 | } 31 | 32 | let targets = {} 33 | let bodies = {} 34 | templ = td.replaceFn(templ, /([^]*?)/g, (elt: string[]) => { 35 | let result: string; 36 | let name = elt[1]; 37 | targets[name] = elt[2]; 38 | bodies[name] = elt[3]; 39 | result = ""; 40 | return result; 41 | }); 42 | let body = (pubdata["body"] || "").replace(/
\s*<\/div>/g, ""); 43 | let s = body.replace(/^\s*]*md-tutorial[^<>]*>/g, ""); 44 | if (s != body) { 45 | body = s.replace(/<\/div>\s*$/g, ""); 46 | } 47 | body = body.replace(/]md-tutorial[^<>]*>\s*<\/div>/g, ""); 48 | let sects = body.split("
1 && sects[0].trim(" \t\n") == "") { 50 | sects.splice(0, 1); 51 | } 52 | else { 53 | let sname = "main"; 54 | if (sects.length == 1) { 55 | sname = "full"; 56 | } 57 | sects[0] = "data-name='" + sname + "' data-arguments='' />" + sects[0]; 58 | } 59 | let sinks = {}; 60 | for (let s2 of sects) { 61 | let coll = (/[^>]*data-name='([^'"<>]*)' data-arguments='([^'"<>]*)'[^>]*>([^]*)/.exec(s2) || []); 62 | let sectjs = {}; 63 | let name1 = decodeURIComponent(coll[1]); 64 | for (let s3 of decodeURIComponent(coll[2]).split(";")) { 65 | let s4 = s3.trim(); 66 | let coll2 = (/^([^=]*)=(.*)/.exec(s4) || []); 67 | if (coll2[1] == null) { 68 | sectjs[s4] = "true"; 69 | } 70 | else { 71 | sectjs[coll2[1]] = coll2[2]; 72 | } 73 | } 74 | sectjs["body"] = coll[3]; 75 | await expandInfo(sectjs); 76 | let b = sectjs["isvolatile"]; 77 | if (b != null && b) { 78 | pubdata["isvolatile"] = true; 79 | } 80 | for (let fn of Object.keys(pubdata)) { 81 | if (!sectjs.hasOwnProperty(fn)) { 82 | sectjs[fn] = td.clone(pubdata[fn]); 83 | } 84 | } 85 | let expanded = ""; 86 | let target = "main"; 87 | let sectTempl = bodies[name1]; 88 | if (sectTempl == null) { 89 | expanded = "
section definition missing: " + htmlQuote(name1) + "
"; 90 | } 91 | else { 92 | target = targets[name1]; 93 | let promos = sectjs["promo"]; 94 | if (promos != null) { 95 | let accum = ""; 96 | for (let promo of promos) { 97 | let jsb = promo["promo"]; 98 | if (jsb == null) { 99 | continue; 100 | } 101 | let replRes = fmt(promo, "
  • \n @promo.name@ by @promo.username@, \n @promo.description@\n
  • "); 102 | if (orEmpty(jsb["link"]) != "") { 103 | replRes = fmt(promo, "
  • \n @promo.name@ by @promo.username@,\n @promo.description@\n
  • "); 104 | } 105 | accum = accum + replRes; 106 | } 107 | sectjs["body"] = orEmpty(sectjs["body"] + accum); 108 | } 109 | expanded = td.replaceFn(sectTempl, /@(\w+)@/g, (elt1: string[]) => { 110 | let result1: string; 111 | let key = elt1[1]; 112 | result1 = orEmpty(sectjs[key]); 113 | if (! /^(body)$/.test(key)) { 114 | result1 = htmlQuote(result1); 115 | } 116 | return result1; 117 | }); 118 | } 119 | sinks[target] = orEmpty(sinks[target]) + expanded; 120 | } 121 | td.jsonCopyFrom(pubdata, td.clone(sinks)); 122 | let expanded1 = td.replaceFn(templ, /@(\w+)(:\w+)?@/g, (mtch: string[]) => { 123 | let val = orEmpty(pubdata[mtch[1]]); 124 | if (mtch[2] == ":hide") { 125 | if (val.trim()) return ""; 126 | else return "display:none;" 127 | } 128 | return val; 129 | }); 130 | return expanded1; 131 | } 132 | 133 | var orEmpty = td.orEmpty; 134 | 135 | export function htmlQuote(s: string): string { 136 | s = td.replaceAll(s, "&", "&") 137 | s = td.replaceAll(s, "<", "<") 138 | s = td.replaceAll(s, ">", ">") 139 | s = td.replaceAll(s, "\"", """) 140 | s = td.replaceAll(s, "\'", "'") 141 | return s; 142 | } 143 | 144 | export function init(expandInfo_: td.Action1): void { 145 | expandInfo = expandInfo_; 146 | } 147 | 148 | /** 149 | * {language:html:html} 150 | */ 151 | function fmt(promo: JsonBuilder, html: string): string { 152 | let replRes = td.replaceFn(html, /@([a-zA-Z0-9_\.]+)@/g, (elt: string[]) => { 153 | let result: string; 154 | let jsb = promo; 155 | for (let fldName of elt[1].split(".")) { 156 | if (jsb == null) { 157 | break; 158 | } 159 | jsb = jsb[fldName]; 160 | } 161 | if (jsb == null) { 162 | result = ""; 163 | } 164 | else { 165 | result = htmlQuote(orEmpty(td.toString(jsb))); 166 | } 167 | return result; 168 | }); 169 | return replRes; 170 | } 171 | 172 | function twoDigit(p: number): string { 173 | let s2 = "00" + p; 174 | return s2.substr(s2.length - 2, 2); 175 | } 176 | 177 | export function humanTime(p: Date): string { 178 | return p.getFullYear() + "-" + twoDigit(p.getMonth() + 1) + "-" + twoDigit(p.getDate()) + 179 | " " + twoDigit(p.getHours()) + ":" + twoDigit(p.getMinutes()); 180 | } 181 | 182 | 183 | -------------------------------------------------------------------------------- /src/tdlite-i18n.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | import * as core from "./tdlite-core" 9 | import * as tdlitePointers from "./tdlite-pointers" 10 | import * as crowdin from "./crowdin" 11 | import * as parallel from "./parallel" 12 | import * as cachedStore from "./cached-store" 13 | 14 | var logger = core.logger; 15 | var httpCode = core.httpCode; 16 | var i18nCache: cachedStore.Container; 17 | 18 | async function downloadCachedTranslationAsync(filename: string, lang: string) { 19 | let key = "i18n:" + filename + ":" + lang 20 | let f = await core.redisClient.getAsync(key) 21 | if (f) 22 | return JSON.parse(f) 23 | let dat = await crowdin.downloadTranslationAsync(filename, lang) 24 | dat = dat || {} 25 | logger.info(`crowdin dl: ${key} - ${Object.keys(dat).length}`) 26 | await core.redisClient.setpxAsync(key, JSON.stringify(dat), 10 * 60 * 1000) 27 | return dat 28 | } 29 | 30 | export async function translateHtmlAsync(html: string, lang: string[]) { 31 | //logger.debug(`crowdin translate html: ${crowdin.enabled} '${lang[0]}' ${html.length}`) 32 | if (!crowdin.enabled || !lang || !lang[0]) return html; 33 | let trdata = await downloadCachedTranslationAsync("website.json", lang[0]) 34 | let res = crowdin.translate(html, trdata) 35 | return res.text 36 | } 37 | 38 | export async function initAsync() { 39 | if (core.hasSetting("CROWDIN_KEY")) 40 | crowdin.init(); 41 | else return; 42 | 43 | core.addRoute("POST", "i18n", "upload", async (req) => { 44 | if (!core.checkPermission(req, "i18n")) 45 | return; 46 | let resp = await crowdin.uploadTranslationAsync(td.toString(req.body["filename"]), req.body["strings"]) 47 | req.response = {}; 48 | }) 49 | 50 | core.addRoute("GET", "i18n", "langs", async (req) => { 51 | req.response = { 52 | defaultLang: core.serviceSettings.defaultLang, 53 | langs: Object.keys(core.serviceSettings.langs) 54 | } 55 | }) 56 | 57 | core.addRoute("POST", "i18n", "pointers", async (req) => { 58 | if (!core.checkPermission(req, "i18n")) 59 | return; 60 | let strs = td.toStringArray(req.body["pointers"]) 61 | let res = {} 62 | await parallel.forJsonAsync(strs, async (fn) => { 63 | let ptr = await core.getPubAsync(fn, "pointer") 64 | if (ptr && ptr["pub"]["htmlartid"]) { 65 | let text = await tdlitePointers.getTemplateTextAsync(fn.replace(/^ptr-/, ""), []) 66 | td.jsonCopyFrom(res, crowdin.translate(text, {}).missing) 67 | } 68 | }, 10) 69 | let phrases = Object.keys(res) 70 | phrases.sort(td.strcmp) 71 | let obj = td.toDictionary(phrases, s => s) 72 | await crowdin.uploadTranslationAsync("website.json", obj) 73 | req.response = { 74 | pointers: strs, 75 | phrases: phrases 76 | } 77 | }); 78 | 79 | core.addRoute("GET", "*pointer", "i18n", async (req) => { 80 | let text = await tdlitePointers.getTemplateTextAsync(req.rootId.replace(/^ptr-/, ""), []) 81 | req.response = crowdin.translate(text, {}).missing; 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /src/tdlite-promos.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | var asArray = td.asArray; 12 | 13 | import * as azureTable from "./azure-table" 14 | import * as parallel from "./parallel" 15 | import * as core from "./tdlite-core" 16 | import * as tdliteScripts from "./tdlite-scripts" 17 | import * as tdliteSearch from "./tdlite-search" 18 | import * as tdliteIndex from "./tdlite-index" 19 | 20 | 21 | var orEmpty = td.orEmpty; 22 | var logger = core.logger; 23 | var httpCode = core.httpCode; 24 | 25 | var promosTable: azureTable.Table; 26 | 27 | export async function initAsync() : Promise 28 | { 29 | promosTable = await core.tableClient.createTableIfNotExistsAsync("promos"); 30 | await tdliteScripts.scripts.createCustomIndexAsync("promo", promosTable); 31 | core.addRoute("GET", "promo-scripts", "*", async (req: core.ApiRequest) => { 32 | await core.anyListAsync(tdliteScripts.scripts, req, "promo", req.verb); 33 | }, { 34 | cacheKey: "promo" 35 | }); 36 | core.addRoute("GET", "promo", "config", async (req1: core.ApiRequest) => { 37 | core.checkPermission(req1, "script-promo"); 38 | if (req1.status != 200) { 39 | return; 40 | } 41 | req1.response = core.getSettings("promo"); 42 | }); 43 | core.addRoute("GET", "*script", "promo", async (req2: core.ApiRequest) => { 44 | core.checkPermission(req2, "script-promo"); 45 | if (req2.status != 200) { 46 | return; 47 | } 48 | req2.response = await getPromoAsync(req2); 49 | }); 50 | core.addRoute("POST", "*script", "promo", async (req3: core.ApiRequest) => { 51 | core.checkPermission(req3, "script-promo"); 52 | if (req3.status != 200) { 53 | return; 54 | } 55 | let langs = Object.keys(core.serviceSettings.langs) 56 | let promo = await getPromoAsync(req3); 57 | let oldPromoId = orEmpty(req3.rootPub["promoId"]); 58 | if (oldPromoId != "") { 59 | for (let l of langs) { 60 | let suff = l == core.serviceSettings.defaultLang ? "" : "@" + l 61 | await parallel.forJsonAsync(promo["tags"], async(json: JsonObject) => { 62 | let entity = azureTable.createEntity(td.toString(json) + suff, oldPromoId); 63 | let ok = await promosTable.tryDeleteEntityAsync(td.clone(entity)); 64 | }); 65 | } 66 | } 67 | let jsb2 = td.clone(promo); 68 | td.jsonCopyFrom(jsb2, req3.body); 69 | let coll = ([]); 70 | let newTags = jsb2["tags"]; 71 | if (newTags.length > 0) { 72 | let d = {}; 73 | for (let jsb3 of newTags) { 74 | d[td.toString(jsb3)] = "1"; 75 | } 76 | d["all"] = "1"; 77 | jsb2["tags"] = td.arrayToJson(Object.keys(d)); 78 | } 79 | promo = td.clone(jsb2); 80 | let offsetHours = Math.round(td.clamp(-200000, 1000000, core.orZero(promo["priority"]))); 81 | let newtime = Math.round(req3.rootPub["pub"]["time"] + offsetHours * 3600); 82 | let newId = (10000000000 - newtime) + "." + req3.rootId; 83 | await tdliteSearch.updateAndUpsertAsync(core.pubsContainer, req3, async (entry: JsonBuilder) => { 84 | entry["promo"] = promo; 85 | entry["promoId"] = newId; 86 | }); 87 | let js = promo["tags"]; 88 | if (core.jsonArrayIndexOf(js, "hidden") >= 0) { 89 | js = (["hidden"]); 90 | } 91 | else if (core.jsonArrayIndexOf(js, "preview") >= 0) { 92 | js = (["preview"]); 93 | } 94 | let lang = core.serviceSettings.defaultLang; 95 | for (let l of langs) { 96 | if (core.jsonArrayIndexOf(js, l) >= 0) { 97 | lang = l; 98 | } 99 | } 100 | let langSuff = lang == core.serviceSettings.defaultLang ? "" : "@" + lang; 101 | await parallel.forJsonAsync(js, async(json1: JsonObject) => { 102 | let tag = td.toString(json1) 103 | if (tag != lang) 104 | tag += langSuff; 105 | let entity1 = azureTable.createEntity(tag, newId); 106 | entity1["pub"] = req3.rootId; 107 | await promosTable.insertEntityAsync(td.clone(entity1), "or merge"); 108 | }); 109 | await core.flushApiCacheAsync("promo"); 110 | req3.response = promo; 111 | }); 112 | } 113 | 114 | async function getPromoAsync(req: core.ApiRequest) : Promise 115 | { 116 | let js2 = req.rootPub["promo"]; 117 | if (js2 == null) { 118 | let jsb = ({ "tags": [], "priority": 0 }); 119 | let lastPtr = await core.getPubAsync(req.rootPub["lastPointer"], "pointer"); 120 | if (lastPtr != null) { 121 | jsb["link"] = "/" + lastPtr["pub"]["path"]; 122 | } 123 | return jsb; 124 | } 125 | return js2; 126 | } 127 | -------------------------------------------------------------------------------- /src/tdlite-status.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | import * as restify from "./restify" 13 | import * as azureTable from "./azure-table" 14 | import * as azureBlobStorage from "./azure-blob-storage" 15 | import * as child_process from 'child_process'; 16 | import * as tdliteIndex from "./tdlite-index" 17 | import * as core from "./tdlite-core" 18 | 19 | var orEmpty = td.orEmpty; 20 | var logger = core.logger; 21 | var httpCode = restify.http(); 22 | 23 | var lastSearchReport: Date; 24 | 25 | 26 | export async function failureReportLoopAsync() : Promise 27 | { 28 | let container = await core.blobService.createContainerIfNotExistsAsync("blobwritetest", "private"); 29 | let table = await core.tableClient.createTableIfNotExistsAsync("tablewritetest"); 30 | lastSearchReport = new Date(); 31 | while (true) { 32 | await td.sleepAsync(300 + td.randomRange(0, 100)); 33 | /* async */ checkSearchAsync(); 34 | await td.sleepAsync(30); 35 | /* async */ doFailureChecksAsync(container, table); 36 | } 37 | } 38 | 39 | 40 | async function checkSearchAsync() : Promise 41 | { 42 | let res = await tdliteIndex.statisticsAsync(); 43 | lastSearchReport = new Date(); 44 | } 45 | 46 | async function doFailureChecksAsync(container: azureBlobStorage.Container, table: azureTable.Table) : Promise 47 | { 48 | if (Date.now() - lastSearchReport.getTime() > 100000) { 49 | logger.tick("Failure@search"); 50 | } 51 | if (await core.redisClient.isStatusLateAsync()) { 52 | logger.tick("Failure@redis"); 53 | } 54 | let result2 = await container.createBlockBlobFromTextAsync(td.randomInt(1000) + "", "foobar", { 55 | justTry: true 56 | }); 57 | if ( ! result2.succeded()) { 58 | logger.tick("Failure@blob"); 59 | } 60 | let entity = azureTable.createEntity(td.randomInt(1000) + "", "foo"); 61 | let ok = await table.tryInsertEntityExtAsync(td.clone(entity), "or replace"); 62 | if ( ! ok) { 63 | logger.tick("Failure@table"); 64 | } 65 | } 66 | 67 | 68 | export async function cpuLoadAsync() : Promise 69 | { 70 | let load: number; 71 | await new Promise(resume => { 72 | child_process.execFile("wmic", ["cpu", "get", "loadpercentage"], function (err, res:string) { 73 | var arr = []; 74 | if (res) 75 | res.replace(/\d+/g, m => { arr.push(parseFloat(m)); return "" }); 76 | load = 0; 77 | arr.forEach(function(n) { load += n }); 78 | load = load / arr.length; 79 | resume(); 80 | }); 81 | }); 82 | return load; 83 | } 84 | 85 | export async function statusReportLoopAsync() : Promise 86 | { 87 | while (true) { 88 | await td.sleepAsync(30 + td.randomRange(0, 10)); 89 | let value = await cpuLoadAsync(); 90 | logger.measure("load-perc", value); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/tdlite-tags.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | import * as indexedStore from "./indexed-store" 13 | import * as core from "./tdlite-core" 14 | 15 | var orEmpty = td.orEmpty; 16 | 17 | var logger = core.logger; 18 | var httpCode = core.httpCode; 19 | var tags2: indexedStore.Store; 20 | 21 | /* ----------------------------------------------------------------------------- 22 | * 23 | * This is really not functional. It's a stub for full TD functiality! 24 | * 25 | * ----------------------------------------------------------------------------- 26 | */ 27 | 28 | export class PubTag 29 | extends core.IdObject 30 | { 31 | @td.json public time: number = 0; 32 | @td.json public name: string = ""; 33 | @td.json public category: string = ""; 34 | @td.json public description: string = ""; 35 | @td.json public instances: number = 0; 36 | @td.json public topscreenshotids: string[]; 37 | static createFromJson(o:JsonObject) { let r = new PubTag(); r.fromJson(o); return r; } 38 | } 39 | 40 | export interface IPubTag { 41 | kind: string; 42 | time: number; 43 | id: string; 44 | url: string; 45 | name: string; 46 | category: string; 47 | description: string; 48 | instances: number; 49 | topscreenshotids: string[]; 50 | } 51 | 52 | async function importTagAsync(req: core.ApiRequest, body: JsonObject) : Promise 53 | { 54 | let grp = new PubTag(); 55 | grp.fromJson(core.removeDerivedProperties(body)); 56 | 57 | let jsb = {}; 58 | jsb["pub"] = grp.toJson(); 59 | jsb["id"] = grp.id; 60 | await tags2.insertAsync(jsb); 61 | } 62 | 63 | function resolveTags(entities: indexedStore.FetchResult) : void 64 | { 65 | let coll = ([]); 66 | for (let jsb of entities.items) { 67 | let tag = PubTag.createFromJson(jsb["pub"]); 68 | tag.topscreenshotids = ([]); 69 | coll.push(tag); 70 | } 71 | entities.items = td.arrayToJson(coll); 72 | } 73 | 74 | export async function initAsync() : Promise 75 | { 76 | tags2 = await indexedStore.createStoreAsync(core.pubsContainer, "tag"); 77 | core.registerPubKind({ 78 | store: tags2, 79 | deleteWithAuthor: false, 80 | importOne: importTagAsync, 81 | }) 82 | await core.setResolveAsync(tags2, async (fetchResult: indexedStore.FetchResult, apiRequest: core.ApiRequest) => { 83 | resolveTags(fetchResult); 84 | }); 85 | core.addRoute("GET", "*script", "tags", async (req: core.ApiRequest) => { 86 | req.response = ({ "items": [] }); 87 | }); 88 | } 89 | 90 | -------------------------------------------------------------------------------- /src/tdlite-ticks.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | 8 | type JsonObject = td.JsonObject; 9 | type JsonBuilder = td.JsonBuilder; 10 | 11 | 12 | import * as core from "./tdlite-core" 13 | 14 | var orEmpty = td.orEmpty; 15 | 16 | var logger = core.logger; 17 | var httpCode = core.httpCode; 18 | 19 | export async function initAsync() 20 | { 21 | core.addRoute("POST", "ticks", "", async(req: core.ApiRequest) => { 22 | await core.throttleAsync(req, "ticks", 30); 23 | if (req.status != 200) return; 24 | let js = req.body["sessionEvents"]; 25 | if (js != null) { 26 | let allowed = core.currClientConfig.tickFilter || {} 27 | for (let evName of Object.keys(js)) { 28 | let tickName = evName.replace(/\|.*/, "") 29 | if (allowed.hasOwnProperty(tickName)) { 30 | logger.customTick("app_" + tickName, { 31 | repeat: td.clamp(0, 100, js[evName]) 32 | }); 33 | } 34 | } 35 | } 36 | req.response = {}; 37 | }, { 38 | noSizeCheck: true 39 | }); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/tdshell.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | 'use strict'; 4 | 5 | import * as td from './td'; 6 | import * as assert from 'assert'; 7 | import * as fs from 'fs'; 8 | import * as zlib from 'zlib'; 9 | import * as path from 'path'; 10 | import * as crypto from 'crypto'; 11 | 12 | export async function sendEncryptedAsync(target:string, path: string, data: any) { 13 | let m = /^(.*\/-tdevmgmt-\/)(\w+)\/?$/.exec(target) 14 | let hash = crypto.createHash("sha256") 15 | hash.update(m[2]); 16 | let key = hash.digest(); 17 | 18 | let op = { 19 | cmd: path.split("/"), 20 | data: data || {}, 21 | op: "ShellMgmtCommand" // should go at the end, for added security 22 | } 23 | 24 | let request = td.createRequest(m[1] + "encrypted"); 25 | 26 | let xbuf = new Buffer(JSON.stringify(op), "utf8"); 27 | var gzipped: Buffer = zlib.gzipSync(xbuf); 28 | console.log("upload " + xbuf.length + " bytes, compressed " + gzipped.length + " [encrypted]") 29 | 30 | let iv = crypto.randomBytes(16); 31 | let cipher = crypto.createCipheriv("aes256", key, iv); 32 | request.setHeader("x-tdshell-iv", iv.toString("hex")); 33 | let enciphered = cipher.update(gzipped); 34 | let cipherFinal = cipher.final(); 35 | request.setContentAsBuffer(Buffer.concat([enciphered, cipherFinal])); 36 | request.setMethod("post"); 37 | let response = await request.sendAsync(); 38 | 39 | let buf = response.contentAsBuffer(); 40 | let inpiv = response.header("x-tdshell-iv"); 41 | if (response.statusCode() == 200) { 42 | let ciph = crypto.createDecipheriv("AES256", key, new Buffer(inpiv, "hex")); 43 | let b0 = ciph.update(buf) 44 | let b1 = ciph.final() 45 | let dec = Buffer.concat([b0, b1]) 46 | let dat = zlib.gunzipSync(dec).toString("utf8"); 47 | (response)._content = dat; 48 | } 49 | 50 | console.log(`${path}: ${response.statusCode() }`) 51 | return response 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "noImplicitAny": false, 5 | "outDir": "built/ts", 6 | "rootDir": "src", 7 | "newLine": "LF", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators":true, 10 | "module": "commonjs", 11 | "sourceMap": false 12 | }, 13 | "exclude": [ 14 | "typings", 15 | "node_modules", 16 | "shell", 17 | "localdir" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "typings", 6 | "bundle": "typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "68d9d43ce884e1ffb6ec0daf4ee7be9392c5b7bc" 10 | }, 11 | "bluebird/bluebird.d.ts": { 12 | "commit": "68d9d43ce884e1ffb6ec0daf4ee7be9392c5b7bc" 13 | }, 14 | "marked/marked.d.ts": { 15 | "commit": "0d622d857f97d44ea7dcad2b3edec1f23c48fe9e" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /typings/marked/marked.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Marked 2 | // Project: https://github.com/chjj/marked 3 | // Definitions by: William Orr 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | interface MarkedStatic { 7 | /** 8 | * Compiles markdown to HTML. 9 | * 10 | * @param src String of markdown source to be compiled 11 | * @param callback Function called when the markdownString has been fully parsed when using async highlighting 12 | * @return String of compiled HTML 13 | */ 14 | (src: string, callback: Function): string; 15 | 16 | /** 17 | * Compiles markdown to HTML. 18 | * 19 | * @param src String of markdown source to be compiled 20 | * @param options Hash of options 21 | * @param callback Function called when the markdownString has been fully parsed when using async highlighting 22 | * @return String of compiled HTML 23 | */ 24 | (src: string, options?: MarkedOptions, callback?: Function): string; 25 | 26 | /** 27 | * @param src String of markdown source to be compiled 28 | * @param options Hash of options 29 | */ 30 | lexer(src: string, options?: MarkedOptions): any[]; 31 | 32 | /** 33 | * Compiles markdown to HTML. 34 | * 35 | * @param src String of markdown source to be compiled 36 | * @param callback Function called when the markdownString has been fully parsed when using async highlighting 37 | * @return String of compiled HTML 38 | */ 39 | parse(src: string, callback: Function): string; 40 | 41 | /** 42 | * Compiles markdown to HTML. 43 | * 44 | * @param src String of markdown source to be compiled 45 | * @param options Hash of options 46 | * @param callback Function called when the markdownString has been fully parsed when using async highlighting 47 | * @return String of compiled HTML 48 | */ 49 | parse(src: string, options?: MarkedOptions, callback?: Function): string; 50 | 51 | /** 52 | * @param options Hash of options 53 | */ 54 | parser(src: any[], options?: MarkedOptions): string; 55 | 56 | /** 57 | * Sets the default options. 58 | * 59 | * @param options Hash of options 60 | */ 61 | setOptions(options: MarkedOptions): MarkedStatic; 62 | 63 | Renderer: { 64 | new(): MarkedRenderer; 65 | } 66 | 67 | Parser: { 68 | new(options: MarkedOptions): MarkedParser; 69 | } 70 | } 71 | 72 | interface MarkedRenderer { 73 | code(code: string, language: string): string; 74 | blockquote(quote: string): string; 75 | html(html: string): string; 76 | heading(text: string, level: number): string; 77 | hr(): string; 78 | list(body: string, ordered: boolean): string; 79 | listitem(text: string): string; 80 | paragraph(text: string): string; 81 | table(header: string, body: string): string; 82 | tablerow(content: string): string; 83 | tablecell(content: string, flags: { 84 | header: boolean, 85 | align: string 86 | }): string; 87 | strong(text: string): string; 88 | em(text: string): string; 89 | codespan(code: string): string; 90 | br(): string; 91 | del(text: string): string; 92 | link(href: string, title: string, text: string): string; 93 | image(href: string, title: string, text: string): string; 94 | text(text: string): string; 95 | } 96 | 97 | interface MarkedParser { 98 | parse(source: any[]): string 99 | } 100 | 101 | interface MarkedOptions { 102 | /** 103 | * Type: object Default: new Renderer() 104 | * 105 | * An object containing functions to render tokens to HTML. 106 | */ 107 | renderer?: MarkedRenderer; 108 | 109 | /** 110 | * Enable GitHub flavored markdown. 111 | */ 112 | gfm?: boolean; 113 | 114 | /** 115 | * Enable GFM tables. This option requires the gfm option to be true. 116 | */ 117 | tables?: boolean; 118 | 119 | /** 120 | * Enable GFM line breaks. This option requires the gfm option to be true. 121 | */ 122 | breaks?: boolean; 123 | 124 | /** 125 | * Conform to obscure parts of markdown.pl as much as possible. Don't fix any of the original markdown bugs or poor behavior. 126 | */ 127 | pedantic?: boolean; 128 | 129 | /** 130 | * Sanitize the output. Ignore any HTML that has been input. 131 | */ 132 | sanitize?: boolean; 133 | 134 | /** 135 | * Use smarter list behavior than the original markdown. May eventually be default with the old behavior moved into pedantic. 136 | */ 137 | smartLists?: boolean; 138 | 139 | /** 140 | * Shows an HTML error message when rendering fails. 141 | */ 142 | silent?: boolean; 143 | 144 | /** 145 | * A function to highlight code blocks. The function takes three arguments: code, lang, and callback. 146 | */ 147 | highlight? (code: string, lang: string, callback?: Function): string; 148 | 149 | /** 150 | * Set the prefix for code block classes. 151 | */ 152 | langPrefix?: string; 153 | 154 | /** 155 | * Use "smart" typograhic punctuation for things like quotes and dashes. 156 | */ 157 | smartypants?: boolean; 158 | } 159 | 160 | declare module "marked" { 161 | export = marked; 162 | } 163 | 164 | declare var marked: MarkedStatic; 165 | -------------------------------------------------------------------------------- /web/_includes/footer.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /web/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ title }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /web/_includes/html-foot.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Touch Develop retirement postponed until June 22, 2019. Sign-in and access to cloud assets to be removed on May 23, 2018.Learn More..

    3 |
    4 | © 2015 Microsoft   5 | privacy and cookies    6 | legal    7 | 8 |
    9 |
    10 | 12 | 13 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /web/_includes/topnav.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /web/_layouts/bare.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "_includes/head.html" %} 5 | {% block headAdd %} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | {% block body %} 13 | {% endblock %} 14 | 15 | {% include "_includes/footer.html" %} 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /web/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "_includes/head.html" %} 5 | {% block headAdd %} 6 | {% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | {% include "_includes/topnav.html" %} 13 | 14 | {% block body %} 15 | {% endblock %} 16 | 17 | {% include "_includes/footer.html" %} 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /web/_layouts/docs.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | 3 | {% block body %} 4 |
    5 | 6 | 29 | 30 |
    31 |
    32 |

    Touch Develop retirement postponed until June 22, 2019. Sign-in and access to cloud assets to be removed on May 23, 2018.Learn More..

    33 | 34 |
    35 | 41 |
    42 | 43 |
    44 | {% block innerbody %} 45 | {% endblock %} 46 |
    47 |
    48 |
    49 |
    50 | 51 |
    52 |
    53 | edit this topic @id@ 54 |
    55 |
    56 | 57 | 58 |
    59 | 60 | 63 | 64 | 65 | 66 | 111 | 112 | {% endblock %} 113 | -------------------------------------------------------------------------------- /web/_layouts/signin.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | 3 | {% block body %} 4 |
    5 | 6 | 41 | 42 | 45 | 46 |
    47 | 48 |
    49 | {% block innerbody %} 50 | {% endblock %} 51 |
    52 |
    53 |
    54 |
    55 | 56 | 57 |
    58 | 59 | {% endblock %} 60 | 61 | -------------------------------------------------------------------------------- /web/_mockup/contents.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/docs.html" %} 2 | {% set title = "Touch Develop docs: Var" %} 3 | {% set description = "Some description" %} 4 | 5 | {% block innerbody %} 6 | 7 | 10 | 11 |
    Welcome to TouchDevelop, a friendly mobile app creation environment. With TouchDevelop you can create apps on your mobile phone, tablet, and PC, and share the apps you create in the cloud.

    contents

    • Getting Started Learn more about TouchDevelop, and create your first app!
    • Language Reference Dig into the details of coding with TouchDevelop.
    • API Reference Explore the TouchDevelop API, and learn more about the services, types, and action types.
    • Tutorials Expand your knowledge with lots of great tutorials, from games to connected data sharing apps.
    • Social Coding Understand how to publish, share, merge your scripts with the TouchDevelop environment.
    • Advanced Topics Move beyond the basics and learn about testing, debugging, and more.
    • Teach Find out more about creating custom tutorials, and using TouchDevelop in the classroom.
    • Games Discover techniques for creating lots of different types of games.
    • FAQ Get answers to your questions with the TouchDevelop FAQ.
    • What's New Read our release notes for news on the latest features and fixes.
    • Blog subscribe and follow the TouchDevelop community blog

    legal information

    By using TouchDevelop you agree to the Cloud Services Agreement with Microsoft Research. Please read our Privacy Statement.
    12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /web/_mockup/publishing.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/docs.html" %} 2 | {% set title = "Touch Develop docs: Var" %} 3 | {% set description = "Some description" %} 4 | 5 | {% block innerbody %} 6 | 7 | 10 | 11 |
    Any script created with TouchDevelop must be published in order to be discoverable by other users.
    While you are editing a script, changes are periodically backed up in your account in the cloud, but that does not make your script available to other people.

    how to publish a script

    • open your script in the script editor
    • open the script explorer view
    • tap on the publish button near the script properties
    picture
    • tap on public or publish hidden
    picture
    • Once the publishing is completed, you will receive a landing page for your script. Your script will also receive a unique script id.
    • At this point, you can also export your script as an app by tapping on the export button.
    picture
    Notice also the special text /sadqkzss under the script name. This text uniquely identifies this particular version of the published script.

    why should I publish my script?

    • be part of the TouchDevelop community and share your work. Other users will be able to comment your script, tweak it and re-publish it or give your hearts and improve your score.
    • you can easily share a published script with friends and family.
    • in order to export your script to an app, you must publish it.

    published scripts are public!

    Do not store passwords or other confidential information in your script code. Everyone will be able to see your script on the Internet forever once it is published.

    hidden scripts

    A script can be hidden from the lists in the app, including new scripts, top scripts, and other searches. However, it can still be accessed through the script identifier.
    You can hide and unhide any script through the script view.

    updates

    If you are working on a script, you might want to publish multiple updates of the same script. As long as you do not change the script name (and you are the author of the previous version), the published version will automatically be considered as the latest version of this script.
    When you are editing a script, your changes are not publicly available until you decide to publish the script.
    hint
    Avoid using version numbers in script names.
    12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /web/_mockup/var.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/docs.html" %} 2 | {% set title = "Touch Develop docs: Var" %} 3 | {% set description = "Some description" %} 4 | {% block innerbody %} 5 | 6 | 11 | 12 |

    var

    13 | 14 |
    15 |
    16 |
    17 |
    A local variable is a symbol that represents a value. One can read or write to a local variable.
    18 |

    declaration

    19 |
    A new local variable is defined by using the assignment := operator. 20 | In the editor, you can either tap on var or the extract to var intellibutton. The keyword var 22 | is automatically added.
    23 |
    24 |
    25 |
    26 |
    27 |
    var x := 28 | 0 29 |
    30 |
    31 |
    32 |
    33 |
    34 |

    reading

    35 |
    Simply use the variable in your code to use its value.
    36 |
    37 |
    38 |
    39 |
    40 |
    x → post to wall
    41 |
    42 |
    43 |
    44 |
    45 |

    update

    46 |
    Use the assignment := operator to save a new value in the variable.
    47 |
    48 |
    49 |
    50 |
    51 |
    x := 2
    52 |
    53 |
    54 |
    55 |
    56 |

    scope

    57 |
    A local variable exists within its definition scope. If the variable is defined in an action, it is available in that 58 | entire action. This is the case for x in this example.
    59 |
    However, if a variable is defined within a nested block of code, like a if, 60 | then it only exits within that block.
    61 |
    62 |
    63 |
    64 |
    65 |
    if false then
    66 |
    67 |
    68 |
    `y` only exists within the `else` block
    69 |
    70 |
    71 |
    var y := 72 | 1 73 |
    74 |
    75 |
    76 |
    else
    77 |
    78 |
    79 |
    `y` does not exist here
    80 |
    81 |
    82 |
    end if
    83 |
    84 |
    85 |
    86 |
    87 | 88 | 89 |
    90 | 101 |
    102 | 103 |
    104 | 105 | {% endblock %} 106 | -------------------------------------------------------------------------------- /web/app/authorize.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Touch Develop - login" %} 3 | 4 | {% block body %} 5 | 14 |
    15 | 16 |

    Authorization request

    17 | 18 |
    19 |
    20 | 21 | 35 | 36 |
    37 | 38 | 110 | 111 | 112 | 113 | {% endblock %} 114 | 115 | 116 | -------------------------------------------------------------------------------- /web/appday.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Microsoft Touch Develop - CCGA" %} 3 | {% set description = "Creative Coding Through Games And Apps!" %} 4 | 5 | {% block body %} 6 |
    7 |
    8 | 9 | 10 |
    11 |

    App Day

    12 |

    An event where students learn to code using their phones, tablets or laptops.

    13 |

    14 | Start here 15 |

    16 |
    17 |
    18 |
    19 | 20 |
    21 |
    22 |
    23 | App Day student picture 24 |
    25 |
    26 | App Day student picture 27 |
    28 |
    29 | App Day student picture 30 |
    31 |
    32 | App Day student picture 33 |
    34 |
    35 |
    36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /web/blog.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Microsoft Touch Develop Blog" %} 3 | {% set description = "Blog of the Touch Develop team" %} 4 | 5 | {% block body %} 6 | 7 | 8 |
    9 |
    10 |

    Tales from the TouchDevelop team
    and its community.

    11 |
    12 |
    13 | 14 |
    15 | 16 | 39 |
    40 |
    41 | 42 |

    Retiring the Windows Phone App

    43 | 44 |

    45 | profile picture 46 | Peli de Halleux 47 | - Wednesday, December 8, 2015

    48 | 49 |

    50 | We've retired the Windows Phone App, here's why. 51 |

    52 |
    53 |
    54 | READ MORE 55 |
    56 |
    57 |
    58 | 59 |

    In-browser compiler for BBC micro:bit

    60 | 61 |

    62 | profile picture 63 | Peli de Halleux 64 | - Wednesday, November 28, 2015

    65 | 66 |

    67 | Bringing in-browser compilation for all editors in the BBC micro:bit project! 68 |

    69 |
    70 |
    71 | READ MORE 72 |
    73 |
    74 |
    75 | 76 |

    Touch Develop in 208 Bits

    77 | 78 |

    79 | profile picture 80 | Michał Moskal 81 | - Monday, October 26, 2015

    82 | 83 |

    84 | Compiling Touch Develop to ARM Thumb machine code for BBC micro:bit. 85 |

    86 |
    87 |
    88 | READ MORE 89 |
    90 |
    91 |
    92 | 93 |

    New collaboration features

    94 | 95 |

    96 | profile picture 97 | Jonathan Protzenko 98 | - Tuesday, February 17, 2015

    99 | 100 |

    101 | In this blog post, I present the new collaboration feature, which allows several people from a common group to work on the same script. 102 |

    103 |
    104 |
    105 | READ MORE 106 |
    107 |
    108 |
    109 | 110 |

    TouchDevelop goes open-source

    111 | 112 |

    113 | profile picture 114 | Michał Moskal 115 | - Tuesday, February 10, 2015

    116 | 117 |

    118 | Microsoft Research is releasing the TouchDevelop web app on GitHub under the MIT license. 119 |

    120 |
    121 |
    122 | READ MORE 123 |
    124 |
    125 |
    126 | 127 |

    Welcome to the community blog!

    128 | 129 |

    130 | profile picture 131 | peli@TouchDevelop 132 | - Friday, January 30, 2015

    133 | 134 |

    135 | Welcome to the TouchDevelop community blog! 136 |

    137 |
    138 |
    139 | READ MORE 140 |
    141 |
    142 |
    143 | 144 |
    145 |
    146 |

    Touch Develop is retiring on May 23, 2018. Learn More..

    147 |
    148 |
    149 |
    150 | {% endblock %} 151 | -------------------------------------------------------------------------------- /web/ccga.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Microsoft Touch Develop - CCGA" %} 3 | {% set description = "Creative Coding Through Games And Apps!" %} 4 | 5 | {% block headAdd %} 6 | 14 | {% endblock %} 15 | 16 | {% block body %} 17 |
    18 |
    19 | 20 | 21 |
    22 |

    Teach Creative Coding! 23 |

    24 |

    Touch Develop retirement postponed until June 22, 2019. Sign-in and access to cloud assets to be removed on May 23, 2018.Learn More..

    25 |

    26 | Want to teach students how to make amazing things and to have a real impact on their world? You can! 27 | The Creative Coding Through Games And Apps (CCGA) curriculum provides everything you need to deliver the course, including teacher prep materials, lesson plans, presentations, student assignments, homework, projects, and tests. 28 |

    29 |

    30 | Download course flyer 31 | Join Teacher Network 32 |

    33 |
    34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 |

    Curriculum

    41 |

    42 | Creative Coding Through Games And Apps is a first semester introduction to computer science course built with Touch Develop! 43 | Includes day-by-day plans for implementing the curriculum as a 6, 9, 12, or 18-week instructor-led course. 44 | Download curriculum (100Mb) 45 |

    46 |
    47 |
    48 |
    49 | 53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 |
    60 |
    61 |
    62 | 66 |
    67 |
    68 |
    69 |

    Preview package

    70 |

    71 | Try out the preview package now! Download the freely available Preview Package that contains a course description, teacher and student guides, and sample unit materials. Learn more about the requirements, objectives and learning goals you can achieve in the enclosed sample lesson unit with Creative Coding Through Games and Apps. 72 | Download evaluation package (83Mb) 73 |

    74 |
    75 |
    76 |
    77 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /web/contact.redir.txt: -------------------------------------------------------------------------------- 1 | https://go.microsoft.com/?linkid=2028325 -------------------------------------------------------------------------------- /web/doc/touchdevelop114x114-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo114.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop144x144-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo144.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop1511x1511-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo1511.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop196x196-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo196.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop200x200-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo200.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop512x512-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo512.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop57x57-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo57.png 2 | -------------------------------------------------------------------------------- /web/doc/touchdevelop72x72-png.redir.txt: -------------------------------------------------------------------------------- 1 | /static/logo/logo72.png 2 | -------------------------------------------------------------------------------- /web/error-template.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "@name@" %} 3 | {% set description = "@name@: @body@" %} 4 | 5 | {% block body %} 6 | 32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 | :( 39 |
    40 | 41 |

    Houston, we have a problem.

    42 | 43 |

    @name@

    44 | 45 |

    46 | You can go back to touchdevelop.com or contact us. 47 |

    48 | 49 |

    @body@

    50 | 51 |
    52 | 53 |
    54 |
    55 | 56 |
    57 | 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /web/i18n-messages.html: -------------------------------------------------------------------------------- 1 |
    Cannot find that user account in the legacy system. Maybe try linking other provider?
    2 |
    This user account was already bound to identity in the new system. Maybe try linking other provider?
    3 |
    This user account was already tied to the new system.
    4 |
    Invalid migration code. Please start over.
    5 |
    We couldn't find this email.
    6 |
    Invalid characters in legacy user ID.
    7 |
    We couldn't find this user ID.
    8 |
    No email associated with that user ID. Please go to www.touchdevelop.com and set your email first.
    9 |
    Invalid characters in legacy email code
    10 |
    The code is invalid; please try again
    11 |
    Whoops! The code doesn't seem right. Keep trying!
    12 |
    We need an activation code here, not user password.
    13 |
    The user account doesn't exist anymore.
    14 |
    This code has already been used.
    15 |
    We need an activation code here, not group code.
    16 |
    Group owner is out of activation credits.
    17 |
    This code cannot be entered here. Sorry.
    18 |
    This nickname is not allowed.
    19 |
    Cannot verify email - no such user.
    20 |
    Cannot verify email - invalid or expired code.
    21 |
    Thank you, your email was updated.
    22 | -------------------------------------------------------------------------------- /web/legal.redir.txt: -------------------------------------------------------------------------------- 1 | https://go.microsoft.com/fwlink/?LinkID=246338 2 | -------------------------------------------------------------------------------- /web/microbituploader-exe.redir.txt: -------------------------------------------------------------------------------- 1 | /static/uploader/Microbit.Uploader.exe 2 | -------------------------------------------------------------------------------- /web/microbituploader-zip.redir.txt: -------------------------------------------------------------------------------- 1 | /static/uploader/Microbit.Uploader.zip 2 | -------------------------------------------------------------------------------- /web/microbituploader.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "micro:bit uploader" %} 3 | {% set description = "Automatic upload of compiled. hex files to micro:bit" %} 4 | 5 | {% block body %} 6 | 7 | 8 | 13 | 21 |
    22 |
    23 | 24 | 25 |
    26 |

    micro:bit uploader v0.8 27 |

    28 |

    29 | Tired of drag and dropping .hex files to your BBC micro:bit? Use the uploader to automate it! 30 |

    31 |

    32 | 33 | Freely available on Windows. 34 | 35 |

    36 |

    37 | Download  38 | Run now 39 |

    40 |
    41 |
    42 |
    43 | 44 |
    45 |
    46 |
    47 |

    Works with any browser!

    48 | 49 |

    50 | The uploader monitors your Downloads folder looking for any .hex file being download. 51 | When it detects a new .hex file, the uploader tries to copy it to all connected BBC micro:bits (it copies it to the MICROBIT drives). 52 | That's it! 53 |

    54 |
    55 |
    56 |

    System Requirements

    57 |
      58 |
    • Windows XP or later
    • 59 |
    • .NET runtime 2.0 or higher
    • 60 |
    • Browser: this application will work for any web browser that is supported by the BBC micro:bit
    • 61 |
    62 |
    63 |
    64 |

    Installation Instructions

    65 |
      66 |
    • Download the Microsoft.Uploader.Microbit.zip file to your local computer.
    • 67 |
    • Unzip the .zip file and copy the Microsoft.Uploader.exe file to your desktop.
    • 68 |
    • Launch the Microsoft.Uploader.exe file before working on your BBC micro:bit
    • 69 |
    70 |
    71 |
    72 |
    73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /web/platforms.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Microsoft Touch Develop - Platforms" %} 3 | {% set description = "Microsoft Touch Develop - Platforms" %} 4 | 5 | {% block body %} 6 | 7 |
    8 | 9 |

    platforms

    10 |

    11 | 12 |

    13 |

    Web app for iPhone, iPad, Android, Windows, Windows Phone, Mac, Linux, Kindle, ...

    14 |

    15 | The Touch Develop Web App runs in most modern browsers. 16 |

    17 |
    18 | Touch Develop uses various HTML5 features to implement APIs. Some features are not supported in all browsers or platforms. 19 |
    20 |

    Windows Phone

    21 |
    22 | The Touch Develop app for Windows Phone is deprecated and will be soon phased out. 23 |
    24 |
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /web/privacy.redir.txt: -------------------------------------------------------------------------------- 1 | https://go.microsoft.com/fwlink/?LinkId=521839 -------------------------------------------------------------------------------- /web/signin/emailcode.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/signin.html" %} 2 | {% set title = "Touch Develop - Sign in" %} 3 | 4 | {% block innerbody %} 5 | 6 |
    7 |

    Waiting for email code

    8 |

    @LEGACYMSG@

    9 | 10 |

    Please enter the code

    11 |
    12 | 15 | 16 | 17 |
    18 |
    19 | 22 | 23 | or 24 | 25 |
    26 |
    27 | 28 | {% endblock %} 29 | 30 | -------------------------------------------------------------------------------- /web/signin/legacy.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/signin.html" %} 2 | {% set title = "Touch Develop - Sign in" %} 3 | 4 | {% block innerbody %} 5 | 6 |
    7 |

    Link accounts?

    8 |

    9 | It looks you have never used this @PROVIDER@ identity at touchdevelop.com (next). 10 |

    11 |

    @LEGACYMSG@

    12 | 13 |

    I already have an account

    14 |
    15 | 18 | 19 |
    20 |
    21 | 24 | 25 |
    26 | 27 |

    I don't have an account yet

    28 |
    29 | 32 | 33 |
    34 |
    35 | 36 | {% endblock %} 37 | 38 | -------------------------------------------------------------------------------- /web/signin/providers.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/signin.html" %} 2 | {% set title = "Touch Develop - Sign in" %} 3 | 4 | {% block innerbody %} 5 | 6 | 41 | 42 |
    43 | 44 |

    Sign in with...

    45 | 46 | 47 |

    48 | By signing in to Touch Develop you agree to the 49 | Cloud Services Agreement 50 | with Microsoft Research. Please read our statement on 51 | Privacy and Cookies. 52 |

    53 | 54 | {% macro provider(id, name, svgpath) %} 55 | 63 | {% endmacro %} 64 | 65 |
    66 | {{ provider("liveid", "Microsoft", "m 69.039375,69.505539 56.061165,0 0,56.094971 -56.061165,0 0,-56.094971 z m -62.140084,0 56.061166,0 0,56.094971 -56.061166,0 0,-56.094971 z m 62.140084,-62.106281 56.061165,0 0,56.094971 -56.061165,0 0,-56.094971 z m -62.140084,0 56.061166,0 0,56.094971 -56.061166,0 0,-56.094971 z") }} 67 | {{ provider("azuread", "Office 365", "m14.133862,100.719666l0,-70.847538l62.672253,-23.162165l34.059967,10.900385l0,95.37096l-34.059967,10.266899l-62.672253,-22.528542l62.672253,7.54171l0,-82.4757l-40.873985,9.536572l0,55.860706l-21.798267,9.536713z") }} 68 | {{ provider("facebook", "Facebook", "m118.476212,7.588371l-103.952428,0c-3.545375,0 -6.434131,2.888756 -6.434131,6.434131l0,103.908658c0,3.545242 2.888756,6.434135 6.434131,6.434135l55.937329,0l0,-45.213898l-15.231789,0l0,-17.639088l15.231789,0l0,-12.955753c0,-15.100464 9.235329,-23.285332 22.672546,-23.285332c6.43412,0 11.992897,0.481464 13.612305,0.700336l0,15.756954l-9.322876,0c-7.309509,0 -8.75396,3.457825 -8.75396,8.578842l0,11.248722l17.464127,0l-2.276047,17.63908l-15.18808,0l0,45.257561l29.763313,0c3.545242,0 6.434135,-2.888779 6.434135,-6.434006l0,-103.996213c0.04377,-3.545375 -2.845116,-6.434131 -6.390366,-6.434131z") }} 69 | {{ provider("google", "Google", "m67.658554,55.196247l0,22.556843l31.525406,0c-2.425606,13.591949 -14.210083,23.496719 -31.525406,23.496719c-19.086544,0 -34.594372,-16.162132 -34.594372,-35.245079c0,-19.086563 15.507828,-35.208921 34.594372,-35.208921c8.603447,0 16.303123,2.964193 22.376129,8.748003l0,0.036156l16.303108,-16.303106c-10.013199,-9.326394 -23.062927,-15.074053 -38.679237,-15.074053c-31.955513,0 -57.83805,25.882538 -57.83805,57.83807s25.882537,57.838081 57.83805,57.838081c33.401512,0 55.52095,-23.496727 55.52095,-56.53672c0,-4.229424 -0.393997,-8.278069 -1.120605,-12.145992l-54.400345,0z") }} 70 | {{ provider("yahoo", "Yahoo!", "M 22.9997,12.4375C 25.5708,12.9345 28.1418,13.4315 30.4478,13.4315C 32.7538,13.4315 34.7948,12.9345 36.8357,12.4375L 64.7859,58.7562L 92.6566,12.5965C 94.9361,13.0736 97.2156,13.5508 99.4951,13.5243C 101.775,13.4977 104.054,12.9676 106.334,12.4375L 70.3919,72.8704L 71.8232,118.195C 69.4377,117.718 67.0522,117.241 64.7727,117.267C 62.4932,117.294 60.3197,117.824 58.1463,118.354L 59.1005,73.3475L 22.9997,12.4375 Z ") }} 71 | {{ provider("github", "GitHub", "M 47.8914,112.6C 48.0945,116.879 48.2976,121.158 48.1148,123.799C 47.932,126.439 47.3633,127.441 46.2598,127.739C 45.1562,128.037 43.5177,127.631 38.3113,125.342C 33.1048,123.054 24.3304,118.883 17.0348,111.192C 9.7393,103.501 3.92269,92.2899 1.32285,80.2386C -1.27699,68.1873 -0.660054,55.2959 4.70211,42.9467C 10.0643,30.5975 20.1717,18.7903 33.1332,11.5056C 46.0948,4.22086 61.9105,1.45853 77.0326,5.00599C 92.1547,8.55346 106.583,18.4107 115.06,28.4851C 123.536,38.5595 126.061,48.851 127.289,58.2722C 128.518,67.6935 128.45,76.2445 125.807,85.1271C 123.163,94.0096 117.944,103.224 111.326,110.187C 104.709,117.15 96.6926,121.862 91.6283,124.489C 86.564,127.116 84.4517,127.658 82.908,127.549C 81.3644,127.441 80.3894,126.683 80.0103,125.708C 79.6311,124.733 79.8478,123.541 79.929,119.858C 80.0103,116.175 79.9561,110 79.7124,106.074C 79.4686,102.147 79.0353,100.468 78.3515,99.0053C 77.6677,97.5429 76.7334,96.2971 75.7991,95.0513C 77.1125,95.0513 78.426,95.0513 80.3827,94.7534C 82.3393,94.4556 84.9391,93.8597 87.539,92.939C 90.1388,92.0182 92.7387,90.7724 95.1489,88.6871C 97.5592,86.6019 99.7799,83.677 101.351,80.8335C 102.921,77.9899 103.842,75.2276 104.411,71.7882C 104.98,68.3488 105.196,64.2324 104.655,60.7118C 104.113,57.1912 102.813,54.2664 101.811,52.2894C 100.809,50.3124 100.105,49.2833 99.6001,48.5321C 99.0952,47.7808 98.7898,47.3073 98.4843,46.8339C 99.1071,44.823 99.73,42.8122 99.7473,40.2054C 99.7647,37.5985 99.1765,34.3957 98.7741,32.5505C 98.3717,30.7053 98.155,30.2178 97.9383,29.7304C 96.2593,29.947 94.5802,30.1637 92.6574,30.597C 90.7346,31.0303 88.5681,31.6802 86.889,32.4927C 85.21,33.3051 84.0184,34.2801 82.9351,35.0113C 81.8518,35.7425 80.8769,36.23 79.902,36.7174C 75.5147,35.58 71.1275,34.4426 65.7112,34.4426C 60.2948,34.4426 53.8494,35.58 47.404,36.7174C 44.6958,35.0925 41.9876,33.4676 40.0107,32.4927C 38.0337,31.5178 36.7879,31.1928 35.4068,30.922C 34.0256,30.6512 32.509,30.4345 30.9925,30.2178C 30.3425,32.0052 29.6925,33.7926 29.3134,35.905C 28.9343,38.0174 28.8259,40.4547 28.9613,42.5129C 29.0968,44.5711 29.4759,46.2502 29.855,47.9292C 28.8801,48.7959 27.9052,49.6625 26.6323,51.6936C 25.3595,53.7247 23.7887,56.9204 23.4638,62.7971C 23.1388,68.6738 24.0596,77.2316 27.9593,83.1354C 31.8591,89.0392 38.7378,92.289 43.3146,93.8598C 47.8914,95.4305 50.1663,95.3222 52.4411,95.2138C 51.412,96.5679 50.3829,97.922 49.5976,99.3032C 48.8122,100.684 48.2706,102.093 47.9727,103.068C 47.6748,104.042 47.6206,104.584 46.3749,104.99C 45.1291,105.397 42.6917,105.667 40.3898,105.451C 38.0879,105.234 35.9213,104.53 34.161,103.311C 32.4007,102.093 31.0466,100.359 29.8009,98.8428C 28.5551,97.3262 27.4177,96.0263 26.1178,95.0243C 24.8178,94.0222 23.3554,93.3181 22.0284,92.8577C 20.7014,92.3973 19.5098,92.1807 18.6161,92.289C 17.7225,92.3973 17.1267,92.8306 17.235,93.4535C 17.3433,94.0764 18.1558,94.8889 19.2932,95.7555C 20.4306,96.6221 21.893,97.5429 23.1117,99.0053C 24.3304,100.468 25.3053,102.472 25.9011,104.015C 26.4969,105.559 26.7136,106.642 27.9864,108.023C 29.2592,109.405 31.5883,111.084 34.0053,112.133C 36.4223,113.183 38.9274,113.602 41.2564,113.575C 43.5854,113.548 45.7384,113.074 47.8914,112.6 Z") }} 72 |
    73 | 74 |

    75 | Touch Develop client apps use cloud services to synchronize your scripts across all your devices. 76 |

    77 |
    78 | 79 | {% endblock %} 80 | 81 | -------------------------------------------------------------------------------- /web/signout.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Touch Develop - Signed out" %} 3 | 4 | {% block body %} 5 | 14 | 15 | 23 | 24 | {% endblock %} 25 | 26 | 27 | -------------------------------------------------------------------------------- /web/static/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "TD Symbols"; 3 | src:url(data:application/x-font-woff;base64,d09GRgABAAAAAA6AAA8AAAAAFIgAAQAKAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABWAAAABwAAAAcbURtWU9TLzIAAAF0AAAAXgAAAGDCr8gvY21hcAAAAdQAAABgAAABepl1ln9jdnQgAAACNAAAAFkAAACiD00YpGZwZ20AAAKQAAAEqQAAB7R+YbYRZ2FzcAAABzwAAAAQAAAAEAAVACNnbHlmAAAHTAAABDgAAAUkRa4K32hlYWQAAAuEAAAAMgAAADYK6LO9aGhlYQAAC7gAAAAdAAAAJBA8BgpobXR4AAAL2AAAACkAAAAqQp4D22xvY2EAAAwEAAAAGAAAABgDRASubWF4cAAADBwAAAAgAAAAIAGNAKluYW1lAAAMPAAAAPYAAAG/vR06CXBvc3QAAA00AAAAVAAAAHuSJebqcHJlcAAADYgAAAD4AAABCUO3lqQAAAABAAAAANIEFAUAAAAAyTUxiwAAAADSCyfIeJxjYGaXYZzAwMrAwTqL1ZiBgVEeQjNfZEhj/MjBxMTNxsbMysLExPKAgem9A4NCNAMDgwYQMxg6BjszMChOUrdnk/8nwtDC0csUocDAOB8kx+LBug1IAbkAnM4OXQAAeJxjYGBgZoBgGQZGBhAoAfIYwXwWhgggLcQgABRhUpykbKm6XfWzmqVatTqfuv3//wwMmGL3Cu+cvuV3S/jmuZu9N37fOA01Ew0wsjHAJRiZgAQTugJsuoYXAAAxgRoQeJxjE2EQZ/Bj3QYkS1m3sZ5lQAEsHgwiDBMZGP6/AfEQ5D8REAnUJfxnyv+3/1r/v/q3Eigi8W8PA1mAA0J1MzQy3GWYwdDP0Mcwk6GDoZGRn6ELAE09H/8AAAB4nHVVz1PbRhTeFQYMGCJTyjDVIatu7MJgl3SStkApbG3J2HXTYgwzK+hBIiZjeuKUQ6ad8a2MSP+XJ3IxOeXaQ/+HHNpbOSbX9L2VTSAz1Qhr3/d+7vfeLmr78CDQ+3vt3dbOTz8++qH5faO+XfO9auU7tbX57cY362urX3/15Rf3Vz4vlxY/KxbuyU/duwtzefvOzPTU5ER2fGw0M2JxVhLAQx9GCiJfi6Qvo3q5JPyFrlcu+bIWgogE4CdTlPW6gWQEIhRQxE90Aw5BoeWTDyxVaqmuLbktNtgGpZAC/vKk6PODlsb1H54MBFyZ9SOzzhSNMI2C66KHqYqqFT7UnnZjP8QaeTI1WZXV48lyiSWTU7icwhUsytOEL25ys7AW/fXEYtlpSos79aMO7LS07zmuG5RLDZiRnlGxqgkJY1UYNyHFCZXOzkVSehU/79vsKFzOdWQn+lnDSIS+8Ygfx79DfhmWpAdLz/5ewJ0fQ0l6PixT1ObudZ7m+5QcRgu2FPEbhtuRV//eRqIBMlaw3zBaglUFvqtdepwach3HNSlqcRhH/Xe9IylsGSe5XHzqI91sR2OI/ruX5w7Ungdgh12+Hgy2XtttwketQw1WoSa6ESL4bkl31XHz1zY7/6dmSAuSgwy7LtFw3lfsCAXotXQqC3bkXDC1shyAFZLm1VDz8T5pekPNtXsosbfNto4hU2h0pI+Mn0fQO8Lp+oUaI22Yeeu4Mp7Ni7WVwNgKrKrROREwWkSS0OumA84NucS2EWbepp8rBxMU87NiTWIYiuNLPxy8T7sLGEAg0fXldBD2NCgPFyoadMxP7q+gRxRiw04800xYkacwJyvX3aWy/JO2Ni4DN5irAgsfD7xgxTfnSvhx6KUlUCzZ0pfswbvXyUPhvHjAHrLAI+P5Kk5Z0Y915wncDZ0OnrsnQjsuqAA7HEh9HNDYIUNLrx0zHIGZlT3dbMtm60CvDgpJFRQuU/A/CCO1k4bBAYRsISu05YwEaGgjIGq4kJUN/IXxQhb/bCTcoDS4lQ2hucOG1lgGLAn/2BvYkXwr6CiNU7U+jDZGIsap1h03cNOnXLJQLQaJ0SNLpNaHKrymUJHF+azWDURcLtDQCy2PZSC7AtSOpr0RPYblARmG80Gv9m5JN8hCmpiL6qFAZEJt2blJLmwb+Vqsf6BuDNUizspmO6bgchCQYeUNYDTCajXvmLuADrTEu1fYeKTNgY4Tpegwd9cpiGx0YtnWG8Ya75PfnGeUa5Y1eXOvUi7h1VZJJD9rJYqftQ/0pc2YONvTFxa3qmElSO6hTl8KxpRBLUIJJEGQQJF2Ucgae+dSMdYz2owBjPy4z5nBskOMs8d9K8XsNFHRJFLMQk0m1aihdQaxbIr1DGaehBFlanJUZdWEylnTlpNwgi4QeckZm+DsRY5PcydBr10D93kvmVBOatFDC5VWeLb/PvX+gX6RY+hmfjFRhR4cl4UuNhv/rfiiQ4Pya9CNw4AOG5vH1uDLgctNbJPcxELGcjApjyswJSuEbxG+leJjhI/jiPJ5ju497P0OcJqAQ+3ikRSf/OnE9hV1KsBLJbb/Kf8HKfchKQAAAAABAAMACAAKAA0AB///AA94nFVTXWzbVBS+99rXbtLGiXEct03axklsp2r+lh87S5Zk7aSq1WBr06JJoElDUVdGUdGgY4WBQAzBWKGMIqBDYgg0CTFtKg+s2ksF2kYlYNLQeEC88TSp40eM9WVt7XAdWqDSvfZ3fM93zvnOuQYQHAM3mCE6DxwAQBFqhpyTNYMZso5Pr6yX6Gsr02jhEdu4sDINAMDgy/oZ5ib+AlCABS7AA5HwMmJYIBvKvKwIFC/zEPMyNbq8TC0vmzexYfaYb7ufZvxrw7i09i186ggVP7KRR2M0+nGtygTv/0INsWiMhAcIMHWakXGEIAyA5IAUD6fpb9aLhF5Fh9HhtdNMcONP25citZwmtVwgng5SC8AkP8lr5ydvqDYSnyNEswfVzI9oNIZ+2sw3SNJhcKH+BHuJmQROokIGcVAiWrJ6Ju3zMuGQqmT1tE9sQOwBchDwjafwf6wbelDySayqqUGWYdErw8VCtVooDqO+6tTICAHrtxEHp+Ax1GKuWjPWm+Zfr8Ov4C5YgkvWHuuadd3q/S2SWUT0Ylp5pkObtZ6cjQYoszg8/GzV5pOAIyM/E77bvNvgr6IWOEVoe+BSI8zX1m5TyWcuX87kB2La7FvRnkZv5us/MDP4DdBKdPUBYKi5bIWMV9BYRvRKvkzakEU5pKkKw4ZFzRAzREouq1GGQDRDXxc0KM3Q1HCIyKI0W+j1mDpa/rW1VGOl8EG3B75vBfbuTOCZhxc+u3VrudDG7vju1D1l4/bnh0YrbXd21UJZlq3kbpy6q+WTrz20f2DvXGTCqkcueoVwMAY/lv2Pvesc/OC9/maDF7ID8NB+62Bfr06fg3PvRKz6RPhiFDomrE/2wfO9sp9oYsEi+BB/jxeAANpAEERBGjwIRsjMyjCXTUIm7GXtbYvTYQW6kaBAe2GYSRNpENgCIQt9GcIw7DmLXpaD9rKHnMsaZSjZx2kNK00tqCh16vqg7/cm1tm+8fJZ19xZ10noQC9eMcfNV3m4hFyogsyr8LzOmOtHEebd5qSry+NycjxmBI7r4DkH56GxN/4SKjc7d0K4uyngxE1/SAO6EWgtomZ0f37ONT8Hn3/APGk+fgW9AB1e817e2YLKG4u67OYRc9RcY9Adl4hpngT2dLk4L6Y5T7PL07nKmVdRH/lTJgFg+vEl0pXjpE/2xDgoen3kYvrShm7Y7SEXVCOQiCZfO8kpQ7yYkLbNULUE1OxubBm5LKEY24x/Imy3GiGoT8+Uyplshqxy6T84XVbyOd1Ixbtj0biSTz7qT8nxaKw7nko/l8wrqQ6MlS0jHk1oakQ6kCyEN12K5WSxu8m/hW0XP93cHkjpJwb7T+ipQHsD9w/aWMh5JMnbo42XC7WE2iZiKZSoFUvjWo9XFNvUHfsU9cAmTtQK5YlwSKRFf8TGmz4xru9frNrcCgB/A6dmMf94nGNgZGBgYJScNWPv4b54fpuvDNwcDCBwiVv9Fpxm+8/CvpYtD8jlYGACiQIAVToLBQAAeJxjYGRg4Oj9uwJIhjMAAftaBkYGVMAFAFnVA1oAAAB4nONggAAOIGbbwlDK3s+whU2dgQ1Ec4QzrGLXYZgBFN/GkAcAc98HuQAAAAAAAAAAAAAAABgASABeAH4A7AFWAeICkgABAAAACwB4AAcAAAAAAAIAEAAvAFwAAAEOAAAAAAAAeJyNjkFKw1AURU+atLUiDsSBw+g8bVLooAUn0nYBVpyn8AmF9AfSdtAluBbBPTh3HW7ADXhNnjjQgR/e/+f9d7n3Aee8ENCekBPjgFNujDv0uTUOuebJOJLm1bjLGe/GPQZBX8ogGjQdxgGXUrXcUW5iHHLHwjiS5tm4yxVvxj0u+MAzIgc/0vXAnJgVR7asqSjZ6XMer47bdVWK73EUHDTIqdW64lDmgqXEnn3z1lI42cxUfxm2k0yrTlVjUvFEFpXfL6u6cPEs/olUkyXTZJxmk3+s96jgWp+bZp2vkKEq1cDVu03l42yYpa3PL5dvk0+oxz/wAAB4nGNgYgCDfyYMaQzYADcQMzIwMTIxMjOyMLIysjGyM3IwcnIlFhXllxdlpmeUsJfmZRoZG1uCaVMncwjtZgymzaDiZuZOYNrcwBVCG7sBAInYFXt4nE2Lu07DQBBFd7xOolRjiLCIwB7zcJrtWPpEKUzCgniYkeJESkVPYVNDg5QmiJav8LrLX/AhFHyCcag4xdU9urqjz+7RJOIIQqaJZAoBw2FYhvLaDOjKaDJJTINzj2N9yv1eTR23pras6XKqadpsPb3LLZDs6uYtAeVQllJeJH36TuBEH/OhPmBf7/EOIHsaGfEGHcIvdBBrdNoOCAYt+Em8iFL8CNcT8OpDCzbwUT2kSplNp743tnu7sLCycbrN0d3ctldW8HwxqwDes7f1WowDY8/SmY2CzNjHpnhB5YtxludKLfPiWW0pVF6o//zp/vIXcTtBBg==); 4 | } 5 | 6 | .symbol { font-family: "TD Symbols"; } 7 | @media screen { 8 | body { 9 | padding-top: 70px; 10 | } 11 | } 12 | img.logo, img.avatar { 13 | margin-top: -7px; 14 | height:32px; 15 | } 16 | img.checker { 17 | background-image: url(/static/images/checker.png); 18 | background-repeat: repeat; 19 | } 20 | div.listing 21 | { 22 | color:Black; 23 | border-style:none; 24 | margin: 0px 0px 0px 0px; 25 | font-size: 11pt; 26 | font-family: Consolas, "Andale Mono WT" , "Andale Mono" , "Lucida Console" , "Lucida Sans Typewriter" , "DejaVu Sans Mono" , "Bitstream Vera Sans Mono" , "Liberation Mono" , "Nimbus Mono L" , Monaco, "Courier New" , Courier, monospace; 27 | padding: 5px 5px 5px 5px; 28 | background: #f8f8f8; 29 | clear: both; 30 | 31 | white-space: pre-wrap; /* css-3 */ 32 | white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ 33 | white-space: -pre-wrap; /* Opera 4-6 */ 34 | white-space: -o-pre-wrap; /* Opera 7 */ 35 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 36 | } 37 | div.listing A:link 38 | { 39 | color:#000020; 40 | text-decoration: none; 41 | } 42 | div.listing A:visited 43 | { 44 | color:#000020; 45 | text-decoration: none; 46 | } 47 | div.listing A:active 48 | { 49 | color:#000080; 50 | text-decoration: none; 51 | } 52 | div.listing A:hover 53 | { 54 | color:#000080; 55 | text-decoration: underline; 56 | } 57 | div.mscc { 58 | 59 | } -------------------------------------------------------------------------------- /web/static/css/touchdevelop.css: -------------------------------------------------------------------------------- 1 | .md-tutorial { font-size: 1.0em; line-height: 1.4em; } 2 | .parse-error, .decl { margin-bottom: 1em; } 3 | .parse-error, .error { color: #d00; } 4 | .hint { color: #444; } 5 | .errorNumber { color: #aaa; } 6 | .kw, .keyword { font-weight: bold; color: black; } 7 | .kbm { display:inline-block;} .kbm svg { height: 1em; vertical-align: middle; margin-left: 0.2em; } 8 | .kbm svg > path.biton { fill:#000; } .kbm svg > path.bitoff { fill:#ccc; } 9 | .code-highlight { border: 1px dashed gray; font-weight:bold; } 10 | .comment { color:#444; font-style:italic; } 11 | .greyed { color:#444; } .greyer { color: #555; } 12 | .api-kind { border: 1px dotted #BBB; padding: 0.4em; clear: both; font-size: 1.2em; margin-bottom: 0.6em; } 13 | .md-snippet { border: 1px dotted #bbb; padding: 0.4em 0; clear: both; line-height: 1.3em; page-break-inside:avoid; } 14 | .md-snippet .signature { padding:0em 0.2em 0em 0.2em; } 15 | .md-snippet .name { font-weight: bold;} 16 | .md-img { margin:0.5em; clear:both; width:100%; text-align:center; position:relative; } 17 | .md-img-inner { position:relative; display:inline-block; width:100%; } 18 | .md-img .caption { font-size:0.8em; } 19 | .md-img img { max-height: 100%; max-width: 100%; width:auto;} 20 | .md-box { page-break-inside: avoid; } 21 | .md-box-header, .md-box-header-print { font-weight: bold; font-size: 1.2em; } 22 | .md-box, .md-box-landscape, .md-box-portrait { margin-left: 2em; margin-bottom:0.5em; border: 1px solid #555; border-left-width: 0.5em; padding: 1em; } 23 | .md-box-avatar { margin-bottom:0.5em; } 24 | .md-box-avatar-img { width:4em; display:inline-block; vertical-align:top;} 25 | .phone .md-box-avatar-img { width:3em; } 26 | .md-box-avatar-body { position:relative; padding:0em 0.5em 0em 0.5em; color:#000; background:#eeeeee; margin-left:1.5em; display:inline-block; width: calc(100% - 7em); } .phone .md-box-avatar-body { width: calc(100% - 6em); } .md-box-avatar-body:after { top: 1.1em; left: -1.5em; bottom: auto; border-width: 0px 1.5em 0.7em 0; border-color: transparent #eeeeee; content: ''; position: absolute; border-style: solid; display: block; width: 0; } 27 | .md-box-card { border: solid 1px black; background: inherit; margin-left:inherit; min-height:4em;} 28 | a.md-api-entry-link, .api-kind a { text-decoration: none; } 29 | .api-desc { color: black; font-size: 0.8em; } 30 | .signature { font-size: 1.3em; } 31 | .md-api-entry { color: black; border: 1px dotted #BBB; padding: 0.6em 1em 0.5em 1em; margin: 0.5em 0; } 32 | .md-api-header { font-size: 1.4em; margin-bottom: 1em; } 33 | .md-api-header .signature { margin-bottom: 0.4em; } 34 | .md-api-header .sig-parameter { display:inline-block; margin-left: 4em; } 35 | .md-para { margin-top: 1em; margin-bottom: 1em; } 36 | .block .md-para { margin: 0; } 37 | span.stringLiteral { word-break: break-all; } 38 | div.stringLiteral { white-space:pre-wrap; } 39 | .picLiteral { height:1em; vertical-align: bottom;} 40 | .md-warning { background-color:lightyellow; border-left:solid 0.25em red; padding:0.5em; margin-left:2em;} 41 | div.md-video-link { position: relative; } 42 | div.md-video-link > img { width: calc(100% - 1em); } 43 | div.md-video-link > svg { position: absolute;left:0em; bottom: 0em; width:25%; } 44 | @media print { div.md-video-link > svg { display:none; } } 45 | @media print { a.md-external-link:link:after, a.md-external-link:visited:after { content: ' (' attr(href) ') '; font-size: 80%; } } 46 | svg.video-play { width: calc(100% - 1em); background-size:cover; background-repeat:no-repeat;} 47 | .nopara .md-para { margin:0; } 48 | .symbol { font-family: "TD Symbols", inherit; } 49 | .placeholder { font-size: 0.7em; padding: 0.2em; border: 1px dotted gray; } 50 | .md-comment, .md-comment h1, .md-comment h2, .md-comment h3, .md-comment h4 { display: inline-block; margin:0; } 51 | code { font-size: 1.0em; font-family: Calibri, "Helvetica Neue", HelveticaNeue, Helvetica, Arial, "TD Symbols", sans-serif; border: 1px dotted #bbb; padding: 0em 0.2em 0.1em 0.2em; white-space:nowrap; } 52 | code.md-ui { border: 2px solid #777; padding-left:0.2em; padding-right:0.2em; white-space:nowrap; } 53 | .block { margin-left: 1em; } 54 | .stmt { border-left: 0.2em solid #aaa; margin-top: 0.2em; } 55 | .line { margin-left: 0.4em; } 56 | .tutorial-step { font-size: 1.7em; } 57 | .tutorial-step .md-comment { font-size: 0.6em; } 58 | .decl > .stmt { border: none; } 59 | .print-big { font-size: 1.5em; } 60 | .md-tutorial a { 61 | font-weight: bold; 62 | } 63 | body { 64 | font-family: "Helvetica Neue", HelveticaNeue, Helvetica, Arial, "TD Symbols", sans-serif; 65 | } 66 | 67 | @media print { 68 | #MicrosoftTranslatorWidget { display: none; } 69 | .body, .inner, .primary-content__inner, .md-tutorial { margin:0; padding:0; } 70 | .md-box-screen { display:none; } 71 | a { color:black; } 72 | .md-tutorial a:link:after, .md-tutorial a:visited:after { content:" (" attr(href) ") "; } 73 | } 74 | @page { 75 | size: auto; 76 | margin: 1cm 1cm 1cm 1cm; 77 | } 78 | 79 | -------------------------------------------------------------------------------- /web/static/images/aaat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/aaat.png -------------------------------------------------------------------------------- /web/static/images/aaat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/aaat2.png -------------------------------------------------------------------------------- /web/static/images/aakr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/aakr.png -------------------------------------------------------------------------------- /web/static/images/amgj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/amgj.png -------------------------------------------------------------------------------- /web/static/images/avpewfrs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/avpewfrs.png -------------------------------------------------------------------------------- /web/static/images/blockeditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/blockeditor.png -------------------------------------------------------------------------------- /web/static/images/botinship_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/botinship_open.png -------------------------------------------------------------------------------- /web/static/images/checker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/checker.png -------------------------------------------------------------------------------- /web/static/images/cvme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/cvme.png -------------------------------------------------------------------------------- /web/static/images/cxld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/cxld.png -------------------------------------------------------------------------------- /web/static/images/cyftmmbj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/cyftmmbj.png -------------------------------------------------------------------------------- /web/static/images/enmn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/enmn.png -------------------------------------------------------------------------------- /web/static/images/geqc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/geqc.png -------------------------------------------------------------------------------- /web/static/images/gkcw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/gkcw.png -------------------------------------------------------------------------------- /web/static/images/html5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/html5.png -------------------------------------------------------------------------------- /web/static/images/hwkuanhf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/hwkuanhf.png -------------------------------------------------------------------------------- /web/static/images/izmtgprs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/izmtgprs.png -------------------------------------------------------------------------------- /web/static/images/kzajxznr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/kzajxznr.png -------------------------------------------------------------------------------- /web/static/images/login-facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/login-facebook.png -------------------------------------------------------------------------------- /web/static/images/login-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/login-google.png -------------------------------------------------------------------------------- /web/static/images/login-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/login-microsoft.png -------------------------------------------------------------------------------- /web/static/images/login-office365.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/login-office365.png -------------------------------------------------------------------------------- /web/static/images/login-yahoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/login-yahoo.png -------------------------------------------------------------------------------- /web/static/images/makecodemc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/makecodemc.jpg -------------------------------------------------------------------------------- /web/static/images/microbit.uploader.screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/microbit.uploader.screenshot.png -------------------------------------------------------------------------------- /web/static/images/microbit.uploader.uploading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/microbit.uploader.uploading.png -------------------------------------------------------------------------------- /web/static/images/mwpv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/mwpv.png -------------------------------------------------------------------------------- /web/static/images/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/play.png -------------------------------------------------------------------------------- /web/static/images/qlgq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/qlgq.png -------------------------------------------------------------------------------- /web/static/images/seie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/seie.png -------------------------------------------------------------------------------- /web/static/images/sftkyutq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/sftkyutq.png -------------------------------------------------------------------------------- /web/static/images/sgic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/sgic.png -------------------------------------------------------------------------------- /web/static/images/stegmfdr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/stegmfdr.png -------------------------------------------------------------------------------- /web/static/images/toplogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/toplogo.png -------------------------------------------------------------------------------- /web/static/images/touchdevelop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/touchdevelop.png -------------------------------------------------------------------------------- /web/static/images/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/user.png -------------------------------------------------------------------------------- /web/static/images/uwzvapre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/uwzvapre.png -------------------------------------------------------------------------------- /web/static/images/wdgn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/wdgn.png -------------------------------------------------------------------------------- /web/static/images/wlmemodx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/wlmemodx.png -------------------------------------------------------------------------------- /web/static/images/yvco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/yvco.png -------------------------------------------------------------------------------- /web/static/images/ywydysip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/images/ywydysip.png -------------------------------------------------------------------------------- /web/static/js/docs.js: -------------------------------------------------------------------------------- 1 | var topicOriginal = '' 2 | var topicTranslated = '' 3 | 4 | function isIE() { 5 | return /trident/i.test(navigator.userAgent); 6 | } 7 | 8 | function dirAuto($el) { 9 | if ($el) { 10 | if (!isIE()) 11 | $el.attr('dir', 'auto'); 12 | else { 13 | var dir = /^[\s\.;:(+0-9]*[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/i.test($el.text()) ? "rtl" : "ltr"; 14 | $el.attr('dir', dir); 15 | } 16 | } 17 | return $el; 18 | } 19 | 20 | function azureSearchSource(query, cb) { 21 | $.ajax({ 22 | url: "https://www.touchdevelop.com/api/pointers?q=feature:@ptr-docs+" + encodeURIComponent(query), 23 | success: function (results) { 24 | cb(results.items); 25 | } 26 | }); 27 | } 28 | 29 | function loadTranslatedDocs(lang) { 30 | $.getJSON('https://tdtutorialtranslator.blob.core.windows.net/docs/' + lang + '/' + topicId, function (res) { 31 | topicTranslated = res.body.join('\n'); 32 | if (!res.manual) topicTranslated += '
    Translations by Microsoft® Translator
    '; 33 | dirAuto($('#contentBody').html(topicTranslated)); 34 | }) 35 | .fail(function (e) { 36 | $.getJSON('https://tdtutorialtranslator.azurewebsites.net/api/translate_doc?scriptId=' + topicId + '&to=' + lang, function (res2) { 37 | topicTranslated = res2.info.body.join('\n'); 38 | if (!res2.info.manual) topicTranslated += '
    Translations by Microsoft® Translator
    '; 39 | dirAuto($('#contentBody').html(topicTranslated)); 40 | }) 41 | }); 42 | } 43 | 44 | function docTranslate() { 45 | var lang = window.navigator.language || window.navigator.userLanguage || "en" 46 | var m = /lang=([a-zA-Z\-]+)/.exec(window.location.href) 47 | if (m) { lang = m[1]; } 48 | lang = lang.slice(0, 2) 49 | if (lang == "zh") lang = "zh-CHS" 50 | if (!lang || /^en/i.test(lang)) { 51 | $('#translateBtnGroup').hide(); 52 | } else { 53 | $('#translateBtnGroup').show(); 54 | var translated = false; 55 | var original = ""; 56 | $('#translateBtn').click(function (e) { 57 | e.preventDefault(); 58 | if (translated) { 59 | dirAuto($('#contentBody').html(original)); 60 | } 61 | else { 62 | if (!original) original = $('#contentBody').html(); 63 | if (topicTranslated) dirAuto($('#contentBody').html(topicTranslated)); 64 | else loadTranslatedDocs(lang); 65 | } 66 | translated = !translated; 67 | }); 68 | } 69 | } 70 | 71 | $(document).ready(function () { 72 | docTranslate(); 73 | $('#searchInput').typeahead({ 74 | minLength: 2, 75 | highlight: false, 76 | hint: true, 77 | }, 78 | { 79 | name: 'docsearch', 80 | source: azureSearchSource, 81 | templates: { 82 | suggestion: function (item) { 83 | var a = document.createElement('a'); 84 | a.href = '/' + item.path; 85 | a.innerText = item.scriptname; 86 | a.title = item.scriptdescription; 87 | return a; 88 | } 89 | } 90 | }); 91 | loadSocialButtons(); 92 | }); 93 | 94 | $(document).ready(function () { 95 | $('.md-video-link').on("click", function () { 96 | var name = $(this).data("playerurl") || $(this).data("videosrc"); 97 | $(this).find("img").remove(); 98 | $(this).find("svg").remove(); 99 | var outer = $('
    ', { 100 | "class": 'embed-responsive embed-responsive-16by9' 101 | }); 102 | outer.appendTo($(this)); 103 | $('"; 9 | } 10 | 11 | d = document.getElementById('tweet') 12 | if (d != null) { 13 | var text = d.title; 14 | var url = d.getAttribute('href', 0); 15 | if (url == null) url = location.href; 16 | d.innerHTML = d.innerHTML 17 | + ""; 18 | } 19 | } 20 | loadSocialButtons(); 21 | -------------------------------------------------------------------------------- /web/static/logo/blockeditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/blockeditor.png -------------------------------------------------------------------------------- /web/static/logo/blockeditorpink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/blockeditorpink.png -------------------------------------------------------------------------------- /web/static/logo/logo114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo114.png -------------------------------------------------------------------------------- /web/static/logo/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo128.png -------------------------------------------------------------------------------- /web/static/logo/logo144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo144.png -------------------------------------------------------------------------------- /web/static/logo/logo1511.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo1511.png -------------------------------------------------------------------------------- /web/static/logo/logo196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo196.png -------------------------------------------------------------------------------- /web/static/logo/logo200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo200.png -------------------------------------------------------------------------------- /web/static/logo/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo48.png -------------------------------------------------------------------------------- /web/static/logo/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo512.png -------------------------------------------------------------------------------- /web/static/logo/logo57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo57.png -------------------------------------------------------------------------------- /web/static/logo/logo72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/logo72.png -------------------------------------------------------------------------------- /web/static/logo/touchdevelop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/touchdevelop.png -------------------------------------------------------------------------------- /web/static/logo/touchdevelopblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/logo/touchdevelopblue.png -------------------------------------------------------------------------------- /web/static/minecraft/touchdevelopmod.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/minecraft/touchdevelopmod.zip -------------------------------------------------------------------------------- /web/static/uploader/Microbit.Uploader.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/uploader/Microbit.Uploader.exe -------------------------------------------------------------------------------- /web/static/uploader/Microbit.Uploader.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/TouchDevelop-backend/ad0557f6f156edf78838038bab75600712ef5105/web/static/uploader/Microbit.Uploader.zip -------------------------------------------------------------------------------- /web/templates/docs.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/docs.html" %} 2 | {% set title = "Touch Develop - @name@" %} 3 | {% set description = "@description@" %} 4 | {% set image = "@image@" %} 5 | 6 | {% block innerbody %} 7 |

    @name@

    8 | @body@ 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /web/templates/docscript.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/script.html" %} 2 | 3 | {% block buttons %} 4 | 5 |
    6 | 11 |
    12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /web/templates/oauth.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "Authorization for script by @name@ /@id@" %} 3 | {% set description = "OAuth page" %} 4 | 5 | {% block body %} 6 |
    7 | 8 |

    authorization for script of @name@ finished

    9 |

    10 | You can close this page. 11 | The Touch Develop script should resume in a different tab or window. 12 |

    13 | 28 |
    29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /web/templates/official.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/docs.html" %} 2 | {% set title = "Touch Develop - @name@" %} 3 | {% set description = "@description@" %} 4 | {% set image = "@image@" %} 5 | 6 | {% block innerbody %} 7 | 10 | 11 |

    @name@

    12 | 13 | @body@ 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /web/templates/script.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/default.html" %} 2 | {% set title = "@name@ by @username@ /@id@" %} 3 | {% set description = "Created with Touch Develop" %} 4 | 5 | {% block body %} 6 | 12 |
    13 |
    14 |
    15 |
    16 |

    @name@

    17 |

    by 18 | Picture of @username@ 19 | @username@ 20 |

    21 |

    @description@

    22 |
    23 | 24 | 27 | 28 |
    29 | 30 | {% block buttons %} 31 |
    32 |
    33 | run 34 |
    35 |
    36 | 37 |
    38 |
    39 | edit 40 |
    41 |
    42 | create new 43 |
    44 |
    45 | {% endblock %} 46 | 47 |
    48 |
    49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /web/templates/tutorial.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/script.html" %} 2 | 3 | {% block buttons %} 4 | 5 |
    6 |
    7 | tutorial 8 |
    9 |
    10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /web/templates/users.html: -------------------------------------------------------------------------------- 1 | {% extends "_layouts/docs.html" %} 2 | {% set title = "Touch Develop - @title@" %} 3 | {% set description = "@description@" %} 4 | {% set image = "@image@" %} 5 | 6 | {% block innerbody %} 7 |

    User-provided content

    8 | @body@ 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /web/touchdevelopmod-zip.redir.txt: -------------------------------------------------------------------------------- 1 | /static/minecraft/touchdevelopmod.zip 2 | -------------------------------------------------------------------------------- /web/trademarks.redir.txt: -------------------------------------------------------------------------------- 1 | https://go.microsoft.com/fwlink/?LinkId=506942 -------------------------------------------------------------------------------- /web/worker-js.html: -------------------------------------------------------------------------------- 1 | var window; 2 | if (typeof window == "undefined") 3 | window = {}; 4 | if (typeof window.location == "undefined") 5 | window.location = {}; 6 | window.isWebWorker = true; 7 | var document; 8 | if (typeof document == "undefined") 9 | document = {}; 10 | window.document = document; 11 | if (typeof window.navigator == "undefined") 12 | window.navigator = self.navigator; 13 | 14 | self.onmessage = function (e) { 15 | var d = e.data; 16 | 17 | if (typeof d != "object") 18 | return; 19 | 20 | 21 | if (d.op == "load") { 22 | if (/^\.\//.test(d.url) || /^https:\/\/az(820584|31353)\.vo\.msecnd\.net\//.test(d.url)) { 23 | console.log("loading " + d.url) 24 | importScripts(d.url) 25 | } 26 | } 27 | }; 28 | 29 | // 30 | --------------------------------------------------------------------------------