1097 строки
45 KiB
TypeScript
1097 строки
45 KiB
TypeScript
///<reference path='refs.ts'/>
|
|
module TDev.RT {
|
|
export module BingServices {
|
|
function readBingSearchResponse(response: WebResponse): BingSearchResult[] {
|
|
var links: BingSearchResult[] = [];
|
|
try {
|
|
var json = response.content_as_json();
|
|
if (json) {
|
|
for (var i = 0; i < json.count(); ++i) {
|
|
var jlink = json.at(i);
|
|
var url = jlink.string('address');
|
|
var name = jlink.string('name');
|
|
var thumb = jlink.string('thumb');
|
|
var web = jlink.string('web');
|
|
if (url != null && url.length > 0)
|
|
links.push({ url: url, name: name, thumbUrl: thumb, web: web });
|
|
}
|
|
}
|
|
return links;
|
|
}
|
|
catch (ex) {}
|
|
return links;
|
|
}
|
|
|
|
export var searchAsync = (kind: string, query: string, loc: Location_): Promise =>
|
|
{
|
|
if (!query) return Promise.as([]);
|
|
|
|
var url = 'runtime/web/search?kind=' + encodeURIComponent(kind) + '&query=' + encodeURIComponent(query);
|
|
if (loc) {
|
|
url += '&latitude=' + encodeURIComponent(loc.latitude().toString()) + '&longitude=' + encodeURIComponent(loc.longitude().toString());
|
|
}
|
|
var request = WebRequest.mk(Cloud.getPrivateApiUrl(url), undefined);
|
|
return request.sendAsync()
|
|
.then((response: WebResponse) => readBingSearchResponse(response));
|
|
}
|
|
}
|
|
|
|
export interface BingSearchResult {
|
|
name: string;
|
|
url: string;
|
|
thumbUrl: string;
|
|
web: string;
|
|
}
|
|
|
|
export interface EventSource {
|
|
addEventListener(msg: string, cb: (e: any) => void, replace: boolean);
|
|
close();
|
|
readyState: number;
|
|
}
|
|
|
|
//? A Server-Sent-Events client
|
|
//@ stem("source")
|
|
export class WebEventSource extends RTDisposableValue {
|
|
private onMessages = {};
|
|
private onOpen: Event_;
|
|
private onError: Event_;
|
|
|
|
constructor(rt : Runtime, private source : EventSource) {
|
|
super(rt);
|
|
}
|
|
|
|
//? Sets an event to run when a message is received. Change name to receive custom events.
|
|
//@ [name].defl('message') ignoreReturnValue writesMutable
|
|
public on_message(name: string, handler: TextAction): EventBinding {
|
|
if (!name || /^close|error$/i.test(name))
|
|
Util.userError(lf("name cannot be 'close' or 'error'"));
|
|
|
|
var onMessage = <Event_>this.onMessages[name];
|
|
if (!onMessage) {
|
|
onMessage = new Event_();
|
|
if (this.source)
|
|
this.source.addEventListener(name, (e: MessageEvent) => {
|
|
if (this.source && onMessage.handlers) {
|
|
var d = e.data || "";
|
|
this.rt.queueLocalEvent(onMessage, [d]);
|
|
}
|
|
}, false);
|
|
}
|
|
return onMessage.addHandler(handler);
|
|
}
|
|
|
|
//? Gets the current connection state (`connecting`, `open`, `closed`)
|
|
public state(): string {
|
|
if (!this.source) return "closed";
|
|
else switch (this.source.readyState) {
|
|
case 0: return "connecting";
|
|
case 1: return "open";
|
|
case 2: return "closed";
|
|
default: return "unkown";
|
|
}
|
|
}
|
|
|
|
//? Sets an event to run when the event source is opened
|
|
//@ ignoreReturnValue writesMutable
|
|
public on_open(opened: Action): EventBinding {
|
|
if (!this.onOpen) {
|
|
this.onOpen = new Event_();
|
|
if (this.source)
|
|
this.source.addEventListener('open', (e) => {
|
|
if (this.source && this.onOpen.handlers)
|
|
this.rt.queueLocalEvent(this.onOpen);
|
|
}, false);
|
|
}
|
|
return this.onOpen.addHandler(opened);
|
|
}
|
|
|
|
//? Sets an event to run when an error occurs
|
|
//@ ignoreReturnValue writesMutable
|
|
public on_error(handler: Action): EventBinding {
|
|
if (!this.onError) {
|
|
this.onError = new Event_();
|
|
if (this.source)
|
|
this.source.addEventListener('error', (e) => {
|
|
if (this.source && this.onError.handlers)
|
|
this.rt.queueLocalEvent(this.onError);
|
|
}, false);
|
|
}
|
|
return this.onError.addHandler(handler);
|
|
}
|
|
|
|
//? Closes the EventSource. No further event will be raised.
|
|
//@ writesMutable
|
|
public close() {
|
|
if (this.source) {
|
|
try {
|
|
this.source.close();
|
|
}
|
|
catch (e) {}
|
|
this.source = undefined;
|
|
}
|
|
}
|
|
|
|
public dispose() {
|
|
this.close();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
//? Search and browse the web...
|
|
export module Web {
|
|
export interface MessageWaiter {
|
|
origin: string;
|
|
handler: (js:JsonObject)=>void;
|
|
}
|
|
|
|
export interface State {
|
|
_onReceivedMessageEvent: Event_;
|
|
_messageWaiters: MessageWaiter[];
|
|
receiveMessage: (event: MessageEvent) => void;
|
|
}
|
|
|
|
export function rt_start(rt: Runtime): void {
|
|
clearReceivedMessageEvent(rt);
|
|
}
|
|
export function rt_stop(rt: Runtime) {
|
|
clearReceivedMessageEvent(rt);
|
|
}
|
|
|
|
function toLink(jlink: BingSearchResult, kind: LinkKind): Link {
|
|
var link: Link = Link.mk(jlink.url, kind);
|
|
var idx = jlink.name.indexOf(' : ');
|
|
if (idx > 0) {
|
|
link.set_title(jlink.name.slice(0, idx));
|
|
link.set_description(jlink.name.slice(idx + 3));
|
|
} else {
|
|
link.set_name(jlink.name);
|
|
}
|
|
return link;
|
|
}
|
|
|
|
function bingSearch(kind: string, query: string, loc: Location_, linkKind: LinkKind, r: ResumeCtx) {
|
|
BingServices.searchAsync(kind, query, loc)
|
|
.done((results: BingSearchResult[]) => {
|
|
var links = Collections.create_link_collection();
|
|
results.forEach(result => {
|
|
links.add(toLink(result, linkKind));
|
|
});
|
|
r.resumeVal(links);
|
|
});
|
|
}
|
|
|
|
//? Searching the web using Bing
|
|
//@ async cap(search) flow(SinkSafe) returns(Collection<Link>)
|
|
//@ [result].writesMutable
|
|
export function search(query: string, r: ResumeCtx) //: Collection<Link>
|
|
{
|
|
bingSearch("Web", query, undefined, LinkKind.hyperlink, r);
|
|
}
|
|
|
|
//? Searching the web near a location using Bing. Distance in meters, negative to ignore.
|
|
//@ async cap(search) flow(SinkSafe) returns(Collection<Link>)
|
|
//@ [result].writesMutable
|
|
//@ [location].deflExpr('senses->current_location') [distance].defl(1000)
|
|
export function search_nearby(query: string, location: Location_, distance: number, r: ResumeCtx) // : Collection<Link>
|
|
{
|
|
bingSearch("Web", query, location, LinkKind.hyperlink, r);
|
|
}
|
|
|
|
//? Searching images using Bing
|
|
//@ async cap(search) flow(SinkSafe) returns(Collection<Link>)
|
|
//@ [result].writesMutable
|
|
export function search_images(query: string, r: ResumeCtx) // : Collection<Link>
|
|
{
|
|
|
|
bingSearch("Images", query, undefined, LinkKind.image, r);
|
|
}
|
|
|
|
//? Searching images near a location using Bing. Distance in meters, negative to ignore.
|
|
//@ async cap(search) flow(SinkSafe) returns(Collection<Link>)
|
|
//@ [result].writesMutable
|
|
//@ [location].deflExpr('senses->current_location') [distance].defl(1000)
|
|
export function search_images_nearby(query: string, location: Location_, distance: number, r: ResumeCtx) // : Collection<Link>
|
|
{
|
|
|
|
bingSearch("Images", query, location, LinkKind.image, r);
|
|
}
|
|
|
|
//? Search phone numbers using Bing
|
|
//@ obsolete cap(search) flow(SinkSafe)
|
|
//@ [result].writesMutable
|
|
export function search_phone_numbers(query: string): Collection<Link> {
|
|
return undefined;
|
|
}
|
|
|
|
//? Search phone numbers near a location using Bing. Distance in meters, negative to ignore.
|
|
//@ obsolete cap(search) flow(SinkSafe)
|
|
//@ [result].writesMutable
|
|
//@ [location].deflExpr('senses->current_location') [distance].defl(1000)
|
|
export function search_phone_numbers_nearby(query: string, location: Location_, distance: number): Collection<Link> {
|
|
return undefined;
|
|
}
|
|
|
|
//? Searching news using Bing
|
|
//@ async cap(search) flow(SinkSafe) returns(Collection<Link>)
|
|
//@ [result].writesMutable
|
|
export function search_news(query: string, r: ResumeCtx) // : Collection<Link>
|
|
{
|
|
bingSearch("News", query, undefined, LinkKind.hyperlink, r);
|
|
}
|
|
|
|
//? Searching news near a location using Bing. Distance in meters, negative to ignore.
|
|
//@ async cap(search) flow(SinkSafe) returns(Collection<Link>)
|
|
//@ [result].writesMutable
|
|
//@ [location].deflExpr('senses->current_location') [distance].defl(1000)
|
|
export function search_news_nearby(query: string, location: Location_, distance: number, r: ResumeCtx) // : Collection<Link>
|
|
{
|
|
bingSearch("News", query, location, LinkKind.hyperlink, r);
|
|
}
|
|
|
|
//? Indicates whether any network connection is available
|
|
export function is_connected(): boolean {
|
|
return window.navigator.onLine;
|
|
}
|
|
|
|
//? Gets the type of the network servicing Internet requests (unknown, none, ethernet, wifi, mobile)
|
|
//@ quickAsync returns(string)
|
|
//@ import("cordova", "org.apache.cordova.network-information")
|
|
export function connection_type(r: ResumeCtx) { //: string
|
|
var res = 'unknown';
|
|
var connection = (<any>navigator).connection;
|
|
if (connection) {
|
|
res = connection.type || 'unknown';
|
|
}
|
|
r.resumeVal(res);
|
|
}
|
|
|
|
//? Gets a name of the currently connected network servicing Internet requests. Empty string if no connection.
|
|
//@ quickAsync returns(string)
|
|
export function connection_name(r : ResumeCtx) { // : string
|
|
r.resumeVal('');
|
|
}
|
|
|
|
//? Opens a connection settings page (airplanemode, bluetooth, wifi, cellular)
|
|
//@ cap(phone) uiAsync
|
|
//@ [page].defl("airplanemode") [page].deflStrings("airplanemode", "bluetooth", "wifi", "cellular")
|
|
export function open_connection_settings(page:string, r : ResumeCtx) : void
|
|
{
|
|
r.resume();
|
|
}
|
|
|
|
//? Opens a web browser to a url
|
|
//@ cap(network) flow(SinkWeb) uiAsync
|
|
export function browse(url: string, r : ResumeCtx): void {
|
|
Web.browseAsync(url).done(() => r.resume());
|
|
}
|
|
|
|
//? Redirects the browser to a url; only available when exporting
|
|
//@ betaOnly uiAsync
|
|
export function redirect(url:string, r: ResumeCtx) : void
|
|
{
|
|
if (r.rt.devMode)
|
|
Util.userError(lf("web->redirect not available when running in the editor"))
|
|
else {
|
|
// TODO this is a hack
|
|
// give it some time to save data etc
|
|
// we never resume
|
|
Util.setTimeout(1500, () => Util.navigateInWindow(url))
|
|
}
|
|
}
|
|
|
|
export var browseAsync = (url: string) =>
|
|
{
|
|
window.open(url, "_blank");
|
|
// popup blocked the url
|
|
return new Promise((onSuccess, onError, onProgress) => {
|
|
var d = new ModalDialog();
|
|
d.onDismiss = () => onSuccess(undefined);
|
|
d.add(div("wall-dialog-header", lf("web browsing...")));
|
|
d.add(div("wall-dialog-body", "We tried to open the following web page: " + url + "."))
|
|
d.add(div("wall-dialog-body", lf("If the page did not open, tap the 'open' button below, otherwise tap 'done'.")))
|
|
d.add(div("wall-dialog-buttons",
|
|
HTML.mkA("button wall-button", url, "_blank", "open"),
|
|
HTML.mkButton(lf("done"), () => {
|
|
d.dismiss();
|
|
})))
|
|
d.show();
|
|
});
|
|
}
|
|
|
|
//? Plays an internet audio/video in full screen
|
|
//@ cap(network) flow(SinkWeb)
|
|
export function play_media(url: string): void {
|
|
window.open(url);
|
|
}
|
|
|
|
//? Creates a link to an internet audio/video
|
|
//@ [result].writesMutable
|
|
export function link_media(url: string): Link { return Link.mk(url, LinkKind.media); }
|
|
|
|
//? Creates a link to an internet image
|
|
//@ [result].writesMutable
|
|
export function link_image(url: string): Link { return Link.mk(url, LinkKind.image); }
|
|
|
|
//? Creates a link to an internet page
|
|
//@ [result].writesMutable
|
|
export function link_url(name: string, url: string): Link {
|
|
var l = Link.mk(url, LinkKind.hyperlink);
|
|
l.set_name(name);
|
|
return l;
|
|
}
|
|
|
|
//? Creates a multi-scale image from an image url
|
|
//@ obsolete
|
|
//@ [result].writesMutable
|
|
export function link_deep_zoom(url: string): Link {
|
|
return undefined;
|
|
}
|
|
|
|
export function proxy(url: string) {
|
|
// don't proxy localhost
|
|
if (!url || /^http:\/\/localhost(:[0-9]+)?\//i.test(url)) return url;
|
|
// don't proxy private ip ranges
|
|
// 10.0.0.0 - 10.255.255.255
|
|
// 172.16.0.0 - 172.31.255.255
|
|
// 192.168.0.0 - 192.168.255.255
|
|
var m = url.match(/^http:\/\/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)(:[0-9]+)?\//i);
|
|
if (m) {
|
|
var a = parseInt(m[1]);
|
|
if (a == 10) return url;
|
|
|
|
var b = parseInt(m[2]);
|
|
if (a == 172 && b >= 16 && b <= 31) return url;
|
|
if (a == 192 && b == 168) return url;
|
|
}
|
|
return Cloud.getPrivateApiUrl("runtime/web/proxy?url=" + encodeURIComponent(url));
|
|
}
|
|
|
|
//? Downloads the content of an internet page (http get)
|
|
//@ async cap(network) flow(SinkWeb) returns(string)
|
|
export function download(url: string, r: ResumeCtx) {
|
|
r.progress('Downloading...');
|
|
var request = create_request(url);
|
|
request
|
|
.sendAsync()
|
|
.done((response: WebResponse) => r.resumeVal(response.content()),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Downloads a web service response as a JSON data structure (http get)
|
|
//@ async cap(network) flow(SinkWeb) returns(JsonObject)
|
|
export function download_json(url: string, r: ResumeCtx) {
|
|
r.progress('Downloading...');
|
|
var request = create_request(url);
|
|
request.set_accept('application/json');
|
|
request
|
|
.sendAsync()
|
|
.done((response: WebResponse) => r.resumeVal(response.content_as_json()),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Downloads a web service response as a XML data structure (http get)
|
|
//@ cap(network) async flow(SinkWeb) returns(XmlObject)
|
|
//@ import("npm", "xmldom", "0.1.*")
|
|
export function download_xml(url: string, r: ResumeCtx) {
|
|
var request = create_request(url);
|
|
r.progress('Downloading...');
|
|
request.set_accept('text/xml');
|
|
request
|
|
.sendAsync()
|
|
.done((response: WebResponse) => r.resumeVal(response.content_as_xml()),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Downloads a WAV sound file from internet
|
|
//@ async cap(network) flow(SinkWeb) returns(Sound)
|
|
export function download_sound(url: string, r: ResumeCtx) // : Sound
|
|
{
|
|
r.progress("Downloading...");
|
|
Sound
|
|
.fromUrl(url)
|
|
.done(snd => r.resumeVal(snd));
|
|
}
|
|
|
|
//? Create a streamed song file from internet (download happens when playing)
|
|
//@ cap(media,network) flow(SinkWeb)
|
|
//@ [name].defl("a song")
|
|
export function download_song(url: string, name: string): Song
|
|
{
|
|
return Song.mk(url, undefined, name);
|
|
}
|
|
|
|
//? Uploads text to an internet page (http post)
|
|
//@ async cap(network) flow(SinkWeb) returns(string)
|
|
export function upload(url:string, body:string, r : ResumeCtx)
|
|
{
|
|
var request = create_request(url);
|
|
r.progress('Uploading...');
|
|
request.set_method('post');
|
|
request.set_content(body);
|
|
request
|
|
.sendAsync()
|
|
.done((response : WebResponse) => r.resumeVal(response.content()),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Uploads a sound to an internet page (http post). The sound must have been recorded from the microphone.
|
|
//@ async cap(network) flow(SinkWeb) returns(string)
|
|
export function upload_sound(url: string, snd: Sound, r : ResumeCtx) //: string
|
|
{
|
|
var request = create_request(url);
|
|
r.progress('Uploading...');
|
|
request.set_method('post');
|
|
request.setContentAsSoundInternal(snd);
|
|
request.sendAsync()
|
|
.done((response : WebResponse) => r.resumeVal(response.content()),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Uploads a picture to an internet page (http post)
|
|
//@ async cap(network) flow(SinkWeb) returns(string)
|
|
export function upload_picture(url: string, pic: Picture, r : ResumeCtx) //: string
|
|
{
|
|
var request = create_request(url);
|
|
r.progress('Uploading...');
|
|
request.set_method('post');
|
|
pic.initAsync()
|
|
.then(() => {
|
|
request.setContentAsPictureInternal(pic, 0.85);
|
|
return request.sendAsync();
|
|
}).done((response : WebResponse) => r.resumeVal(response.content()),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Downloads a picture from internet
|
|
//@ async cap(network) flow(SinkWeb) returns(Picture)
|
|
//@ [result].writesMutable
|
|
export function download_picture(url:string, r : ResumeCtx)
|
|
{
|
|
r.progress('Downloading...');
|
|
var pic = undefined;
|
|
Picture.fromUrl(url)
|
|
.then((p : Picture) => {
|
|
pic = p;
|
|
return p.initAsync();
|
|
})
|
|
.done(() => r.resumeVal(pic),
|
|
(e) => r.resumeVal(undefined));
|
|
}
|
|
|
|
//? Decodes a string that has been HTML-encoded
|
|
export function html_decode(html: string): string
|
|
{
|
|
return Util.htmlUnescape(html);
|
|
}
|
|
|
|
//? Converts a text string into an HTML-encoded string
|
|
export function html_encode(text: string): string
|
|
{
|
|
return Util.htmlEscape(text);
|
|
}
|
|
|
|
//? Decodes a URI component
|
|
export function decode_uri(url: string): string { return decodeURI(url); }
|
|
|
|
//? Encodes a uri component
|
|
export function encode_uri(text: string): string { return encodeURI(text); }
|
|
|
|
//? Decodes a URI component
|
|
export function decode_uri_component(url: string): string { return decodeURIComponent(url); }
|
|
|
|
//? Encodes a uri component
|
|
export function encode_uri_component(text: string): string { return encodeURIComponent(text); }
|
|
|
|
//? Use `web->decode uri component` instead.
|
|
//@ obsolete
|
|
export function url_decode(url:string) : string { return decodeURIComponent(url); }
|
|
|
|
//? Use `web->encode uri component` instead.
|
|
//@ obsolete
|
|
export function url_encode(text:string) : string { return encodeURIComponent(text); }
|
|
|
|
//? Parses the string as a json object
|
|
export function json(value: string): JsonObject
|
|
{
|
|
return JsonObject.mk(value, function (msg) {
|
|
App.logEvent(App.DEBUG, 'json', lf("error parsing json: {0}", msg), undefined);
|
|
});
|
|
}
|
|
|
|
//? Returns an empty json object
|
|
export function json_object(): JsonObject {
|
|
return JsonObject.wrap({});
|
|
}
|
|
|
|
//? Returns an empty json array
|
|
export function json_array(): JsonObject {
|
|
return JsonObject.wrap([]);
|
|
}
|
|
|
|
//? Parses the string as a xml element
|
|
//@ import("npm", "xmldom", "0.1.*")
|
|
export function xml(value:string) : XmlObject
|
|
{
|
|
return XmlObject.mk(value);
|
|
}
|
|
|
|
//? Obsolete. Use 'feed' instead.
|
|
//@ obsolete
|
|
//@ [result].writesMutable
|
|
export function rss(value: string): Collection<Message>
|
|
{
|
|
return feed(value);
|
|
}
|
|
|
|
//? Creates a web request
|
|
export function create_request(url: string): WebRequest
|
|
{
|
|
return WebRequest.mk(url, "text");
|
|
}
|
|
|
|
//? Creates a web socket
|
|
//@ dbgOnly async returns(WebSocket_)
|
|
export function open_web_socket(url: string, r:ResumeCtx)
|
|
{
|
|
var ws = new WebSocket(url)
|
|
ws.onopen = () => {
|
|
ws.onerror = null;
|
|
ws.onopen = null;
|
|
r.resumeVal(WebSocket_.mk(ws, r.rt))
|
|
}
|
|
ws.onerror = () => {
|
|
r.resumeVal(undefined)
|
|
}
|
|
}
|
|
|
|
//? Decodes a string that has been base64-encoded
|
|
export function base64_decode(text: string): string
|
|
{
|
|
return Util.base64Decode(text);
|
|
}
|
|
|
|
//? Converts a string into an base64-encoded string
|
|
export function base64_encode(text: string): string
|
|
{
|
|
return Util.base64Encode(text);
|
|
}
|
|
|
|
function htmlToPictureUrl(value: string) {
|
|
if (!value) return value;
|
|
var m = value.match(/<img.*?src=['"](.*?)['"].*?\/?>/i);
|
|
if (m) return m[1];
|
|
return null;
|
|
}
|
|
|
|
function htmlToText(value : string)
|
|
{
|
|
if (!value) return value;
|
|
|
|
var r = value.replace(/<[^>]+>/ig, '');
|
|
var decoded = Web.html_decode(r);
|
|
var decodedescaped = decoded.replace(/<[^>]+>/ig, '');
|
|
return decodedescaped;
|
|
}
|
|
|
|
function parseRss(rssx: XmlObject, msgs: Collection<Message>)
|
|
{
|
|
var channel = rssx.child('channel');
|
|
if (!channel) return;
|
|
|
|
var imgRx = /^image\/(png|jpg|jpeg)$/i;
|
|
var mediaRx = Browser.isGecko ? /^video\/webm|audio\/mp3$/i : /^video\/mp4|audio\/mp3$/i;
|
|
var items = channel.children('item');
|
|
for (var i = 0; i < items.count(); ++i) {
|
|
var item = items.at(i);
|
|
var msg = Message.mk("");
|
|
msg.set_from(undefined);
|
|
msgs.add(msg);
|
|
var description = item.child('description');
|
|
if (description)
|
|
parseHtmlContent(msg, description.value());
|
|
var title = item.child('title');
|
|
if (title)
|
|
msg.set_title(htmlToText(title.value()));
|
|
var link = item.child('link');
|
|
if (link)
|
|
msg.set_link(link.value());
|
|
var pubDate = item.child('pubDate');
|
|
if (pubDate)
|
|
msg.set_time(DateTime.parse(pubDate.value()));
|
|
var speaker = item.child('{http://www.microsoft.com/dtds/mavis/}speaker');
|
|
if (speaker)
|
|
msg.set_from(htmlToText(speaker.value()));
|
|
var author = item.child('{http://www.itunes.com/dtds/podcast-1.0.dtd}author');
|
|
if (author)
|
|
msg.set_from(htmlToText(author.value()));
|
|
var creator = item.child('creator');
|
|
if (creator)
|
|
msg.set_from(htmlToText(creator.value()));
|
|
var img = item.child('{http://www.itunes.com/dtds/podcast-1.0.dtd}image');
|
|
if (img && img.attr('href'))
|
|
msg.set_picture_link(img.attr('href'));
|
|
var enclosure = item.child('enclosure');
|
|
if (enclosure) {
|
|
var enclosureType = enclosure.attr('type') || "";
|
|
if (imgRx.test(enclosureType))
|
|
msg.set_picture_link(enclosure.attr('url'));
|
|
else if (mediaRx.test(enclosureType))
|
|
msg.set_media_link(enclosure.attr('url'));
|
|
}
|
|
var thumbnails = item.children('{http://search.yahoo.com/mrss/}thumbnail');
|
|
var tisize = -1;
|
|
for (var ti = 0; ti < thumbnails.count(); ++ti) {
|
|
var thumbnail = thumbnails.at(ti);
|
|
var cisize = (String_.to_number(thumbnail.attr('width')) || 1) * (String_.to_number(thumbnail.attr('height')) || 1);
|
|
if (thumbnail.attr('url') && cisize > tisize) {
|
|
msg.set_picture_link(thumbnail.attr('url'));
|
|
tisize = cisize;
|
|
}
|
|
}
|
|
var group = item.child('{http://search.yahoo.com/mrss/}group');
|
|
if (group) {
|
|
var contents = group.children('{http://search.yahoo.com/mrss/}content');
|
|
var mcsize = 0;
|
|
for (var ci = 0; ci < contents.count(); ++ci) {
|
|
var content = contents.at(ci);
|
|
var csize = String_.to_number(content.attr('fileSize')) || 0;
|
|
var curl = content.attr('url');
|
|
var ctype = content.attr('type');
|
|
if (curl && mediaRx.test(ctype) && csize > mcsize) {
|
|
msg.set_media_link(curl);
|
|
mcsize = csize;
|
|
}
|
|
}
|
|
}
|
|
parseGeoRss(msg, item);
|
|
}
|
|
}
|
|
|
|
function parseGeoRss(msg: Message, item: XmlObject) {
|
|
var point = item.child('{http://www.georss.org/georss}point');
|
|
if (point) {
|
|
var txt = point.value();
|
|
var i = txt.indexOf(' ');
|
|
if (i > 0) {
|
|
var lat = parseFloat(txt.substr(0, i));
|
|
var long = parseFloat(txt.substr(i + 1));
|
|
if (!isNaN(lat) && !isNaN(long))
|
|
msg.set_location(Location_.mkShort(lat, long));
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseProperties(msg: Message, entry: XmlObject) {
|
|
var properties = entry.child('{http://schemas.microsoft.com/ado/2007/08/dataservices/metadata}properties');
|
|
if (properties) {
|
|
var props = properties.children("");
|
|
for (var j = 0; j < props.count(); ++j) {
|
|
var prop = props.at(j);
|
|
msg.values().set_at(prop.local_name(), prop.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseHtmlContent(msg: Message, value: string) {
|
|
if (!value) return;
|
|
|
|
msg.set_message(htmlToText(value));
|
|
var pic = htmlToPictureUrl(value);
|
|
if (pic)
|
|
msg.set_picture_link(pic);
|
|
}
|
|
|
|
function parseAtom(feed: XmlObject, msgs: Collection<Message>)
|
|
{
|
|
var channelTitlex = feed.child('title');
|
|
var channelTitle = channelTitlex ? channelTitlex.value() : "atom";
|
|
var entries = feed.children('entry');
|
|
if (entries) {
|
|
for (var i = 0; i < entries.count(); ++i) {
|
|
var entry = entries.at(i);
|
|
var msg = Message.mk("");
|
|
msg.set_from(undefined);
|
|
msgs.add(msg);
|
|
var summary = entry.child('summary');
|
|
if (summary)
|
|
parseHtmlContent(msg, summary.value());
|
|
var title = entry.child('title');
|
|
if (title)
|
|
msg.set_title(htmlToText(title.value()));
|
|
var updated = entry.child('updated');
|
|
if (updated) {
|
|
var updatedd = DateTime.parse(updated.value());
|
|
if (updatedd)
|
|
msg.set_time(updatedd);
|
|
}
|
|
var content = entry.child('content');
|
|
if (content) {
|
|
var contentType = content.attr('type') || "";
|
|
if (/^image\//i.test(contentType) && content.attr('src'))
|
|
msg.set_picture_link(content.attr('src'));
|
|
else if (/^application\/xml/i.test(contentType))
|
|
parseProperties(msg, content);
|
|
else if (/html/i.test(contentType))
|
|
parseHtmlContent(msg, content.value());
|
|
}
|
|
var author = entry.child('author');
|
|
if (author) {
|
|
var authorName = author.child('name');
|
|
if (authorName)
|
|
msg.set_from(authorName.value());
|
|
}
|
|
parseGeoRss(msg, entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
//? Parses the newsfeed string (RSS 2.0 or Atom 1.0) into a message collection
|
|
//@ [result].writesMutable
|
|
//@ import("npm", "xmldom", "0.1.*")
|
|
export function feed(value: string): Collection<Message>
|
|
{
|
|
var msgs = Collections.create_message_collection();
|
|
var xml = XmlObject.mk(value);
|
|
while(xml != null) {
|
|
if (xml.name() === 'rss') {
|
|
parseRss(xml, msgs);
|
|
break;
|
|
}
|
|
else if (xml.name() === 'feed') {
|
|
parseAtom(xml, msgs);
|
|
break;
|
|
}
|
|
else {
|
|
xml = xml.next_sibling();
|
|
}
|
|
}
|
|
return msgs;
|
|
}
|
|
|
|
//? Creates a json builder
|
|
//@ [result].writesMutable
|
|
export function create_json_builder(): JsonBuilder {
|
|
return new (<any>JsonBuilder)(); //TS9
|
|
}
|
|
|
|
//? Parses a Command Separated Values document into a JsonObject where the `headers` is a string array of column names; `records` is an array of rows where each row is itself an array of strings. The delimiter is inferred if not specified.
|
|
//@ [delimiter].defl("\t")
|
|
export function csv(text: string, delimiter : string): JsonObject {
|
|
var file = new CsvParser().parse(text, delimiter);
|
|
return JsonObject.wrap(file);
|
|
}
|
|
|
|
//? Creates a picture from a web address. The resulting picture cannot be modified, use clone if you want to change it.
|
|
export function picture(url: string): Picture {
|
|
return Picture.fromUrlSync(url, true);
|
|
}
|
|
|
|
interface OAuthRedirect {
|
|
redirect_url: string;
|
|
user_id: string;
|
|
time: number;
|
|
}
|
|
|
|
//? Authenticate with OAuth 2.0 and receives the access token or error. See [](/oauthv2) for more information on which Redirect URI to choose.
|
|
//@ cap(network) flow(SinkWeb) returns(OAuthResponse) uiAsync
|
|
export function oauth_v2(oauth_url: string, r : ResumeCtx)
|
|
{
|
|
// validating url
|
|
if (!oauth_url) {
|
|
r.resumeVal(OAuthResponse.mkError("access_denied", "Empty oauth url.", null));
|
|
return;
|
|
}
|
|
// dissallow state and redirect uris.
|
|
if (/state=|redirect_uri=/i.test(oauth_url)) {
|
|
r.resumeVal(OAuthResponse.mkError("access_denied", "The `redirect_uri` and `state` query arguments are not allowed.", null));
|
|
return;
|
|
}
|
|
|
|
// check connection
|
|
if (!Web.is_connected()) {
|
|
r.resumeVal(OAuthResponse.mkError("access_denied", "No internet connection.", null));
|
|
return;
|
|
}
|
|
var userid = r.rt.currentAuthorId;
|
|
|
|
Web.oauth_v2_async(oauth_url, userid).done((v) => {
|
|
r.resumeVal(v);
|
|
})
|
|
}
|
|
|
|
export function oauth_v2_async(oauth_url: string, userid: string): Promise
|
|
// : OAuthResponse
|
|
{
|
|
var redirectURI = "https://www.touchdevelop.com/" + userid + "/oauth";
|
|
var state = Util.guidGen();
|
|
var stateArg = "state=" + state.replace('-', '');
|
|
|
|
var hostM = /^([^\/]+:\/\/[^\/]+)/.exec(document.URL)
|
|
var host = hostM ? hostM[1] : ""
|
|
if (host && !/\.touchdevelop\.com$/i.test(host)) {
|
|
redirectURI = host + "/api/oauth"
|
|
userid = "web-app"
|
|
}
|
|
|
|
var actualRedirectURI = redirectURI;
|
|
// special subdomain scheme?
|
|
var subdomainRx = /&tdredirectdomainid=([a-z0-9]{1,64})/i;
|
|
var msubdomain = oauth_url.match(subdomainRx);
|
|
if (msubdomain) {
|
|
var appid = msubdomain[1];
|
|
actualRedirectURI = 'https://' + appid + '-' + userid + '.users.touchdevelop.com/oauth';
|
|
redirectURI = "https://www.touchdevelop.com/" + appid + '-' + userid + "/oauth";
|
|
App.log('oauth appid redirect: ' + appid);
|
|
oauth_url = oauth_url.replace(subdomainRx, '');
|
|
}
|
|
// state variable needed in redirect uri?
|
|
var stateRx = /&tdstateinredirecturi=true/i;
|
|
if (stateRx.test(oauth_url)) {
|
|
actualRedirectURI += "?" + stateArg;
|
|
Time.log('oauth adding state to url');
|
|
oauth_url = oauth_url.replace(stateRx, '');
|
|
}
|
|
|
|
App.log('oauth login uri: ' + oauth_url);
|
|
App.log('oauth redirect uri: ' + actualRedirectURI);
|
|
// craft url login;
|
|
var url = oauth_url
|
|
+ (/\?/.test(oauth_url) ? '&' : '?')
|
|
+ 'redirect_uri=' + encodeURIComponent(actualRedirectURI)
|
|
+ "&" + stateArg;
|
|
if (!/response_type=token/i.test(url))
|
|
url += "&response_type=token";
|
|
|
|
App.log('oauth auth url: ' + url);
|
|
return Web.oauth_v2_dance_async(url, actualRedirectURI, userid, stateArg);
|
|
}
|
|
|
|
export function oauth_v2_dance_async(url: string, redirect_uri: string, userid: string, stateArg: string): Promise {
|
|
var res = new PromiseInv();
|
|
|
|
var response: OAuthResponse;
|
|
var oauthWindow: Window;
|
|
var m = new ModalDialog();
|
|
|
|
function handleMessage(event) {
|
|
var origin = document.URL.replace(/(.*:\/\/[^\/]+).*/, (a, b) => b)
|
|
if (event.origin == origin && Array.isArray(event.data)) {
|
|
processRedirects(event.data)
|
|
}
|
|
}
|
|
|
|
function handleStorage(event) {
|
|
// console.log("Storage: " + event.key)
|
|
if (event.key == "oauth_redirect")
|
|
processRedirects(JSON.parse(event.newValue || "[]"))
|
|
}
|
|
|
|
function dismiss() {
|
|
window.removeEventListener("message", handleMessage, false)
|
|
window.removeEventListener("storage", handleStorage, false)
|
|
|
|
if (!response)
|
|
response = OAuthResponse.mkError("access_denied", "The user cancelled the authentication.", null);
|
|
if (oauthWindow)
|
|
oauthWindow.close();
|
|
res.success(response);
|
|
}
|
|
|
|
function processRedirects(redirects:OAuthRedirect[])
|
|
{
|
|
redirects.reverse(); // pick the latest oauth message
|
|
|
|
var matches = redirects.filter(redirect => userid == redirect.user_id && redirect.redirect_url.indexOf(stateArg) > -1);
|
|
if (matches.length > 0) {
|
|
Time.log('oauth redirect_uri: ' + matches[0].redirect_url);
|
|
response = OAuthResponse.parse(matches[0].redirect_url);
|
|
m.dismiss();
|
|
return;
|
|
}
|
|
|
|
// is the window still opened?
|
|
if (!response && oauthWindow && oauthWindow.closed) {
|
|
response = OAuthResponse.mkError("access_denied", "The authentication window was closed", null);
|
|
m.dismiss();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// monitors local storage for the url
|
|
function tracker() {
|
|
if (response) return; // we've gotten a response or the user dismissed
|
|
|
|
window.localStorage.setItem("last_oauth_check", Date.now() + "")
|
|
|
|
// array of access tokens
|
|
processRedirects(JSON.parse(window.localStorage.getItem("oauth_redirect") || "[]"))
|
|
|
|
if (!response)
|
|
Util.setTimeout(100, tracker);
|
|
}
|
|
|
|
// start the oauth dance...
|
|
var woptions = 'menubar=no,toolbar=no';
|
|
oauthWindow = window.open(url, '_blank', woptions);
|
|
|
|
m.add(div('wall-dialog-header', 'authenticating...'));
|
|
m.add(div('wall-dialog-body', 'A separate window with the sign in dialog has opened, please sign in in that window.'));
|
|
m.add(div('wall-dialog-body', "Can't see any window? Try tapping the button below to log in manually."));
|
|
m.add(div('wall-dialog-buttons', HTML.mkA("button wall-button", url, "_blank", "log in")));
|
|
m.onDismiss = () => { dismiss(); };
|
|
|
|
m.show();
|
|
|
|
// and start listening...
|
|
Util.setTimeout(100, tracker);
|
|
// window.addEventListener("message", handleMessage, false)
|
|
// window.addEventListener("storage", handleStorage, false)
|
|
|
|
return res;
|
|
}
|
|
|
|
//? Create a form builder
|
|
//@ [result].writesMutable
|
|
export function create_form_builder(): FormBuilder { return new FormBuilder(); }
|
|
|
|
//? Posts a message to the parent window if any. The `target origin` must match the domain of the parent window, * is not accepted.
|
|
//@ readsMutable [target_origin].defl('https://www.touchdevelop.com')
|
|
export function post_message_to_parent(target_origin: string, message: JsonObject, s:IStackFrame) {
|
|
if (!target_origin || target_origin == "*")
|
|
Util.userError(lf("target origin cannot be empty or *"));
|
|
|
|
if (isWebWorker) {
|
|
var msg = message.value()
|
|
if (s && s.rt.pluginSlotId) {
|
|
msg = Util.jsonClone(msg)
|
|
msg.tdSlotId = s.rt.pluginSlotId
|
|
}
|
|
|
|
(<any>self).postMessage(msg)
|
|
return
|
|
}
|
|
|
|
var parent = window.parent;
|
|
if (parent && parent != window && parent.postMessage) {
|
|
try {
|
|
parent.postMessage(message.value(), target_origin);
|
|
}
|
|
catch (e) {
|
|
App.log("web: posting message to parent failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
function receiveMessage(rt:Runtime, event: MessageEvent) {
|
|
var s = rt.webState
|
|
if (window != window.parent && event.source === window.parent && s._onReceivedMessageEvent) {
|
|
App.log("web: receiving message from parent");
|
|
var json = JsonObject.wrap(event.data)
|
|
|
|
var waiters = s._messageWaiters.filter(m => m.origin === event.origin)
|
|
if (waiters.length > 0) {
|
|
// remove them
|
|
s._messageWaiters = s._messageWaiters.filter(m => m.origin !== event.origin)
|
|
waiters.forEach(w => w.handler(json))
|
|
}
|
|
|
|
if (s._onReceivedMessageEvent.handlers) {
|
|
rt.queueLocalEvent(s._onReceivedMessageEvent, [json], false, false, (binding) => {
|
|
var origin = <string>binding.data;
|
|
return event.origin === origin;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export function receiveWorkerMessage(rt:Runtime, data:any) {
|
|
var s = rt.webState
|
|
if (s._onReceivedMessageEvent) {
|
|
var json = JsonObject.wrap(data)
|
|
|
|
var waiters = s._messageWaiters
|
|
if (waiters.length > 0) {
|
|
// remove them
|
|
s._messageWaiters = []
|
|
waiters.forEach(w => w.handler(json))
|
|
}
|
|
|
|
if (s._onReceivedMessageEvent.handlers) {
|
|
rt.queueLocalEvent(s._onReceivedMessageEvent, [json], false, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
function installReceiveMessage(rt:Runtime, origin:string)
|
|
{
|
|
if (!origin)
|
|
Util.userError(lf("origin cannot be empty"));
|
|
var s = rt.webState
|
|
if (!s._onReceivedMessageEvent) {
|
|
s._onReceivedMessageEvent = new Event_();
|
|
s._messageWaiters = [];
|
|
if (!isWebWorker) {
|
|
s.receiveMessage = (e) => receiveMessage(rt, e)
|
|
window.addEventListener("message", s.receiveMessage, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//? Waits for the next message from the parent window in `origin`.
|
|
//@ async
|
|
//@ returns(JsonObject)
|
|
export function wait_for_message_from_parent(origin: string, r: ResumeCtx) {
|
|
installReceiveMessage(r.rt, origin)
|
|
r.rt.webState._messageWaiters.push({
|
|
origin: origin,
|
|
handler: (j) => r.resumeVal(j)
|
|
})
|
|
}
|
|
|
|
//? Attaches code to run when a message is received. Only messages from the parent window and `origin` will be received.
|
|
//@ ignoreReturnValue
|
|
export function on_received_message_from_parent(origin: string, received: JsonAction, s:IStackFrame): EventBinding {
|
|
installReceiveMessage(s.rt, origin)
|
|
var st = s.rt.webState
|
|
var binding = st._onReceivedMessageEvent.addHandler(received);
|
|
binding.data = origin;
|
|
return binding;
|
|
}
|
|
|
|
function clearReceivedMessageEvent(rt:Runtime) {
|
|
var s = rt.webState
|
|
if (s._onReceivedMessageEvent) {
|
|
if (s.receiveMessage)
|
|
window.removeEventListener("message", s.receiveMessage, false);
|
|
s._onReceivedMessageEvent = undefined;
|
|
s._messageWaiters = undefined;
|
|
}
|
|
}
|
|
|
|
//? Opens an Server-Sent-Events client on the given URL. If not supported, returns invalid. The server must implement CORS to allow https://www.touchdevelop.com to receive messages.
|
|
// [result].writesMutable
|
|
export function create_event_source(url: string, s: IStackFrame): WebEventSource {
|
|
if (!url)
|
|
Util.userError(lf("url cannot be empty"), s.pc);
|
|
if (!!(<any>window).EventSource) {
|
|
var source = new (<any>window).EventSource(url);
|
|
return new WebEventSource(s.rt, source);
|
|
} else {
|
|
// Result to xhr polling :(
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
//? Parses a OAuth v2.0 access token from a redirect uri as described in http://tools.ietf.org/html/rfc6749. Returns invalid if the url does not contain an OAuth token.
|
|
export function oauth_token_from_url(redirect_url : string) : OAuthResponse {
|
|
return OAuthResponse.parse(redirect_url);
|
|
}
|
|
|
|
//? Parses a OAuth v2.0 access token from a JSON payload as described in http://tools.ietf.org/html/rfc6749. Returns invalid if the payload is not an OAuth token.
|
|
export function oauth_token_from_json(response: JsonObject): OAuthResponse {
|
|
return OAuthResponse.parseJSON(response);
|
|
}
|
|
}
|
|
}
|