Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 19 commits
  • 14 files changed
  • 0 comments
  • 1 contributor
3  Makefile
... ... @@ -1,6 +1,7 @@
1 1 NAME = webshell.exe
2 2
3   -SRC = parser.opa editor.opa webshell.opa login.opa facebook.opa search.opa dropbox.opa config.opa
  3 +SRC = service.opa editor.opa config.opa login.opa webshell.opa \
  4 + calc.opa facebook.opa search.opa dropbox.opa twitter.opa
4 5 SRCS = $(SRC:%=src/%)
5 6
6 7 all: $(NAME)
0  README
No changes.
15 README.markdown
Source Rendered
... ... @@ -0,0 +1,15 @@
  1 +Webshell
  2 +--------
  3 +
  4 +Webshell is, well, a web shell :). It allows command line access to a number of services including:
  5 +
  6 +* [Blekko][1] - to search
  7 +* [Dropbox][2] - to manage cloud storage
  8 +* [Facebook][3] - for social interactions
  9 +* [Twitter][4] - for microblogging
  10 +* Simple Calculator - for simple calculations
  11 +
  12 +[1] blekko.com
  13 +[2] dropbox.com
  14 +[3] facebook.com
  15 +[4] twitter.com
BIN  resources/img/facebook_signin.png
76 resources/style.css
... ... @@ -1,30 +1,46 @@
1   - #Body {
2   - background-color: #444;
3   - }
4   - #terminal {
5   - border: 1px solid gray;
6   - padding: 4px;
7   - margin-top: 80px;
8   - overflow-y: scroll;
9   - width: 100%;
10   - height: 500px;
11   - font-family: courier, monospace;
12   - color: white;
13   - font-size: 14px;
14   - background-color: black;
15   - }
16   - .search-index {
17   - color: yellow;
18   - }
19   - .search-title {
20   - color: red;
21   - }
22   - .search-pubDate {
23   - color: dimgrey;
24   - }
25   - .username {
26   - color: lime;
27   - }
28   - .prompt {
29   - color: #0BC;
30   - }
  1 +#Body {
  2 + background-color: #444;
  3 +}
  4 +#terminal {
  5 + border: 1px solid gray;
  6 + padding: 4px;
  7 + margin-top: 80px;
  8 + overflow-y: scroll;
  9 + width: 100%;
  10 + height: 500px;
  11 + font-family: courier, monospace;
  12 + color: white;
  13 + font-size: 14px;
  14 + background-color: black;
  15 +}
  16 +.search-index {
  17 + color: yellow;
  18 +}
  19 +.search-title {
  20 + color: red;
  21 +}
  22 +.search-pubDate {
  23 + color: dimgrey;
  24 +}
  25 +.userbox {
  26 + float: right;
  27 +}
  28 +.username {
  29 + color: lime;
  30 +}
  31 +.prompt {
  32 + color: #0BC;
  33 +}
  34 +pre {
  35 + color: white !important;
  36 + display: block !important;
  37 + white-space: pre !important;
  38 + font-family: monospace !important;
  39 + background-color: transparent !important;
  40 + padding: 0px !important;
  41 + margin: 0px !important;
  42 + line-height: 14px;
  43 +}
  44 +.fn-type-folder {
  45 + color: #0BC;
  46 +}
45 src/calc.opa
... ... @@ -0,0 +1,45 @@
  1 +// license: AGPL
  2 +// (c) MLstate, 2011, 2012
  3 +// author: Adam Koprowski, Henri Binsztok
  4 +
  5 +module Calc {
  6 +
  7 + function ws(p) {
  8 + parser { case Rule.ws res=p Rule.ws: res }
  9 + }
  10 +
  11 + `(` = ws(parser { case "(": void })
  12 + `)` = ws(parser { case ")": void })
  13 +
  14 + term = parser {
  15 + case f = {ws(Rule.float)}: f
  16 + case `(` ~expr `)`: expr
  17 + }
  18 +
  19 + factor = parser {
  20 + case ~term "*" ~factor : term * factor
  21 + case ~term "/" ~factor : term / factor
  22 + case ~term : term
  23 + }
  24 +
  25 + expr = parser {
  26 + case ~factor "+" ~expr : factor + expr
  27 + case ~factor "-" ~expr : factor - expr
  28 + case ~factor : factor
  29 + }
  30 +
  31 + Service.spec spec =
  32 + { initial_state: void,
  33 + metadata: {
  34 + id: "calc",
  35 + description: "Simple calculator",
  36 + cmds: [ { cmd: "[EXPR]", description: "Calculates the result of the given expression, using operators: +, -, *, / and parenthesses" } ]
  37 + },
  38 + function parse_cmd(_) {
  39 + parser {
  40 + case ~expr : Service.respond_with(<>= {expr}</>)
  41 + }
  42 + }
  43 + }
  44 +
  45 +}
198 src/dropbox.opa
@@ -9,9 +9,9 @@ database Dropbox.conf /dropbox_config
9 9
10 10 // TODO? could the generic OAuth authentication be bundled in that module?
11 11 // only providing a single simple function?
12   -type Dropbox.credentials = {no_credentials}
13   - or {string request_secret, string request_token}
14   - or {Dropbox.creds authenticated}
  12 +type Dropbox.status = {no_credentials}
  13 + or {string request_secret, string request_token}
  14 + or {Dropbox.creds authenticated, string path}
15 15
16 16 module DropboxConnect {
17 17
@@ -48,86 +48,152 @@ Please re-run your application with: --dropbox-config option")
48 48
49 49 private redirect = "http://{Config.host}/connect/dropbox"
50 50
51   - private creds = UserContext.make(Dropbox.credentials {no_credentials})
52   -
53   - private function set_auth_data(data) {
54   - UserContext.change(function(_) { data }, creds)
55   - }
56   -
57   - private function get_auth_data() {
58   - UserContext.execute(identity, creds)
59   - }
60   -
61   - private function authentication_failed() {
62   - Log.info("Dropbox", "authentication failed")
63   - }
64   -
65   - function connect(data) {
66   - Log.info("Dropbox", "connection data: {data}")
67   - match (get_auth_data()) {
  51 + function login(executor)(raw_token) {
  52 + function connect(auth_data) {
  53 + Log.info("Dropbox", "connection data: {raw_token}")
  54 + authentication_failed = {no_credentials}
  55 + match (auth_data) {
68 56 case ~{request_secret, request_token}:
69   - match (DB.OAuth.connection_result(data)) {
70   - case {success: s}:
71   - if (s.token == request_token) {
72   - match (DB.OAuth.get_access_token(s.token, request_secret, s.verifier)) {
73   - case {success: s}:
74   - dropbox_creds = {token: s.token, secret: s.secret}
75   - Log.info("Dropbox", "got credentials: {dropbox_creds}")
76   - {authenticated: dropbox_creds} |> set_auth_data
77   - default:
78   - authentication_failed()
79   - }
80   - } else
81   - authentication_failed()
82   - default:
83   - authentication_failed()
  57 + match (DB.OAuth.connection_result(raw_token)) {
  58 + case {success: s}:
  59 + if (s.token == request_token) {
  60 + match (DB.OAuth.get_access_token(s.token, request_secret, s.verifier)) {
  61 + case {success: s}:
  62 + dropbox_creds = {token: s.token, secret: s.secret}
  63 + Log.info("Dropbox", "got credentials: {dropbox_creds}")
  64 + {authenticated: dropbox_creds, path: "/"}
  65 + default:
  66 + authentication_failed
  67 + }
  68 + } else
  69 + authentication_failed
  70 + default:
  71 + authentication_failed
84 72 }
85 73 default:
86   - authentication_failed()
  74 + authentication_failed
  75 + }
87 76 }
  77 + executor(connect)
88 78 }
89 79
90   - function authenticate() {
  80 + private function authenticate() {
91 81 token = DB.OAuth.get_request_token(redirect)
92 82 Log.info("Dropbox", "Obtained request token {token}")
93 83 match (token) {
94   - case {success: token}:
95   - auth_url = DB.OAuth.build_authorize_url(token.token, redirect)
96   - {request_secret: token.secret, request_token: token.token} |> set_auth_data
97   - Client.goto(auth_url)
98   - none
99   - default:
100   - Log.error("Dropbox", "authorization failed")
101   - none
  84 + case {success: token}:
  85 + auth_url = DB.OAuth.build_authorize_url(token.token, redirect)
  86 + auth_state = {request_secret: token.secret, request_token: token.token}
  87 + { response: {redirect: auth_url},
  88 + state_change: {new_state: auth_state}
102 89 }
  90 + default:
  91 + Service.respond_with(<>Dropbox authorization failed</>)
  92 + }
103 93 }
104 94
105   - exposed function get_creds() {
106   - match (get_auth_data()) {
107   - case {authenticated: data}: some(data)
108   - default:
109   - authenticate();
110   - none
  95 + private function pad(length, s) {
  96 + String.pad_left(" ", length, s)
  97 + }
  98 +
  99 + private date_printer = Date.generate_printer("%Y-%m-%d %k:%M")
  100 +
  101 + private function show_element(path, Dropbox.element element) {
  102 + function get_name(fname) {
  103 + drop_prefix = if (String.has_prefix("/", path)) path else "/{path}"
  104 + if (String.has_prefix(drop_prefix, fname))
  105 + String.drop_left(String.length(drop_prefix), fname)
  106 + else
  107 + fname
111 108 }
  109 + (info, fname, size) =
  110 + match (element) {
  111 + case {file, ~metadata, ...}:
  112 + Log.info("DB", "path: {path}, filename: {metadata.path}");
  113 + name = get_name(metadata.path)
  114 + (metadata, <>{name}</>, "{metadata.size}")
  115 + case {folder, ~metadata, ...}:
  116 + name = <span class="fn-type-folder">{get_name(metadata.path)}</>
  117 + (metadata, name, "")
  118 + }
  119 + final_size = size |> pad(10, _)
  120 + show_date = Date.to_formatted_string(date_printer, _)
  121 + modification = Option.map(show_date, info.modified) ? ""
  122 + <pre>{final_size} {modification} {fname}</>
112 123 }
113 124
114   - xhtml =
115   - WBootstrap.Button.make(
116   - { button:
117   - <span>Dropbox</>
118   - , callback: function(_) { ls() }
119   - },
120   - []
121   - )
  125 + private function files_to_xhtml(files, path) {
  126 + <>{List.map(show_element(path, _), files)}</>
  127 + }
122 128
123   - function ls() {
124   - match (get_creds()) {
125   - case {some: creds}:
126   - acc_info = DB.Account.info(creds)
127   - Log.info("Dropbox", "Account info: {acc_info}")
128   - default:
129   - authentication_failed()
  129 + function sanitize_url(url) {
  130 + String.replace(" ", "%20", url) // shouldn't this be taken care of somewhere in stdlib?
  131 + }
  132 +
  133 + function normalize_path(path) {
  134 + recursive function aux(segs) {
  135 + match (segs) {
  136 + case []: []
  137 + case ["." | xs]: aux(xs)
  138 + case [x, ".." | xs]: aux(xs)
  139 + case [x | xs]: [x | aux(xs)]
  140 + }
  141 + }
  142 + String.explode_with("/", path, true)
  143 + |> aux
  144 + |> List.to_string_using("", "/", "/", _)
  145 + }
  146 +
  147 + function path_available(path) {
  148 + true // FIXME
  149 + }
  150 +
  151 + function ls(state) {
  152 + match (state) {
  153 + case {authenticated: creds, ~path}:
  154 + db_files = DB.Files("dropbox", sanitize_url(path)).metadata(DB.default_metadata_options, creds)
  155 + response =
  156 + match (db_files) {
  157 + case {success: {~contents, ...}}: files_to_xhtml(contents ? [], path)
  158 + default: <>Dropbox connection failed</>
  159 + }
  160 + Service.respond_with(response)
  161 + default:
  162 + authenticate()
  163 + }
  164 + }
  165 +
  166 + function cd(state, cd_path) {
  167 + match (state) {
  168 + case {authenticated: creds, ~path}:
  169 + new_path = normalize_path("{path}/{cd_path}")
  170 + if (path_available(new_path))
  171 + { response: {outcome: <></>},
  172 + state_change: {new_state: {authenticated: creds, path: new_path}}
  173 + }
  174 + else
  175 + Service.respond_with(<>cd: {cd_path}: No such file or directory</>)
  176 + default:
  177 + authenticate()
130 178 }
131 179 }
132 180
  181 + Service.spec spec =
  182 + { initial_state: Dropbox.status {no_credentials},
  183 + metadata: {
  184 + id: "dropbox",
  185 + description: "Managing Dropbox file storage",
  186 + cmds: [
  187 + { cmd: "ls", description: "Lists contents of the current directory" },
  188 + { cmd: "cd [path]", description: "Change to given directory" }
  189 + ],
  190 + },
  191 + function parse_cmd(state) {
  192 + parser {
  193 + case "ls": ls(state)
  194 + case "cd" Rule.ws path=(.*) : cd(state, Text.to_string(path))
  195 + }
  196 + }
  197 + }
  198 +
133 199 }
24 src/editor.opa
@@ -70,28 +70,32 @@ client module LineEditor {
70 70 #postcaret="";
71 71 }
72 72
  73 + function showStatus(_status) {
  74 + void
  75 + }
  76 +
73 77 function evalKeyPress(echo, event) {
74 78 match ((event.key_modifiers,event.key_code)) {
75   - case (_,{none}): #status = "KeyPress not captured";
  79 + case (_,{none}): showStatus("KeyPress not captured");
76 80 case ([],{some: 8}): void;
77 81 case ([],{some: 13}): void;
78   - case (_,{some: key}): #status = "Key: {key}"; addChar(echo, event, key);
  82 + case (_,{some: key}): showStatus("Key: {key}"); addChar(echo, event, key);
79 83 }
80 84 }
81 85
82 86 function evalKeyDown(callback, event) {
83 87 match (event.key_code) {
84   - case {none}: #status = "KeyDown not captured";
85   - case {some: 8}: #status = "Backspace"; deleteChar();
  88 + case {none}: showStatus("KeyDown not captured")
  89 + case {some: 8}: showStatus("Backspace"); deleteChar();
86 90 case {some: 13}:
87   - #status = "Enter";
  91 + showStatus("Enter");
88 92 //Capture.unset();
89 93 callback(get());
90   - case {some: 37}: #status = "Left"; move({left});
91   - case {some: 38}: #status = "Up"; move({up});
92   - case {some: 39}: #status = "Right"; move({right});
93   - case {some: 40}: #status = "down"; move({down});
94   - case {some: key}: #status = "Key: {key} discarded"; void;
  94 + case {some: 37}: showStatus("Left"); move({left});
  95 + case {some: 38}: showStatus("Up"); move({up});
  96 + case {some: 39}: showStatus("Right"); move({right});
  97 + case {some: 40}: showStatus("down"); move({down});
  98 + case {some: key}: showStatus("Key: {key} discarded"); void;
95 99 }
96 100 }
97 101
69 src/facebook.opa
@@ -13,6 +13,9 @@ type FacebookConnect.user =
13 13
14 14 database Facebook.config /facebook_config
15 15
  16 +type Facebook.status = {no_credentials}
  17 + or {FbAuth.token authenticated}
  18 +
16 19 module FacebookConnect
17 20 {
18 21
@@ -63,29 +66,67 @@ Please re-run your application with: --fb-config option")
63 66 }
64 67 }
65 68
  69 + private auth_url = FBA.user_login_url([{publish_stream}], redirect)
  70 +
66 71 xhtml =
67   - login_url = FBA.user_login_url([], redirect)
68   - WBootstrap.Button.make(
69   - { button:
70   - <img style="width:18px; height:18px; vertical-align:top;" title="Facebook" src="https://opalang.org/sso/img/fb-icon.png" alt="Connect with Facebook" />
71   - <span>Facebook</>
72   - , callback: function(_) { Client.goto(login_url) }
73   - },
74   - []
75   - )
  72 + <a onclick={function (_) { Client.goto(auth_url) }}>
  73 + <img src="resources/img/facebook_signin.png" />
  74 + </>
76 75
77   - function login(token) {
78   - match (FBA.get_token_raw(token, redirect)) {
  76 + function login(executor)(token) {
  77 + function connect(_) {
  78 + match (FBA.get_token_raw(token, redirect)) {
79 79 case {~token}:
80 80 fb_user = { ~token, name: get_fb_name(token) }
81   - {~fb_user}
82   - case {error:_}: {guest}
  81 + Login.set_current_user({~fb_user});
  82 + {authenticated: token}
  83 + case {error:_}: {no_credentials}
  84 + }
83 85 }
84   - |> Login.set_current_user
  86 + executor(connect)
85 87 }
86 88
87 89 function get_name(user) {
88 90 user.name
89 91 }
90 92
  93 + private function authenticate() {
  94 + { response: {redirect: auth_url},
  95 + state_change: {no_change}
  96 + }
  97 + }
  98 +
  99 +
  100 + function publish_status(state, status) {
  101 + match (state) {
  102 + case {authenticated: creds}:
  103 + feed = { Facebook.empty_feed with message: status }
  104 + outcome = FbGraph.Post.feed(feed, creds.token)
  105 + response =
  106 + match (outcome) {
  107 + case {~success}: <>Successfully published Facebook feed item: «{feed.message}»</>
  108 + case {~error}: <>Error: <b>{error.error}</b>; {error.error_description}</>
  109 + }
  110 + Service.respond_with(response)
  111 + default:
  112 + authenticate()
  113 + }
  114 + }
  115 +
  116 + Service.spec spec =
  117 + { initial_state: Facebook.status {no_credentials},
  118 + metadata: {
  119 + id: "facebook",
  120 + description: "Managing Facebook account",
  121 + cmds: [
  122 + { cmd: "fbstatus [content]", description: "Publishes a given Facebook status" }
  123 + ],
  124 + },
  125 + function parse_cmd(state) {
  126 + parser {
  127 + case "fbstatus" Rule.ws content=(.*) : publish_status(state, Text.to_string(content))
  128 + }
  129 + }
  130 + }
  131 +
91 132 }
65 src/parser.opa
... ... @@ -1,65 +0,0 @@
1   -// license: AGPL
2   -// (c) MLstate, 2011, 2012
3   -// author: Adam Koprowski, Henri Binsztok
4   -
5   -client module Calc {
6   -
7   - function int_of_string(string str) {
8   - (option(int)) Parser.try_parse(Rule.integer, str);
9   - }
10   -
11   - function nat_of_string(string str) {
12   - (option(int)) Parser.try_parse(Rule.natural, str);
13   - }
14   -
15   - function ws(p) {
16   - parser { case Rule.ws res=p Rule.ws: res }
17   - }
18   -
19   - `(` = ws(parser { case "(": void })
20   - `)` = ws(parser { case ")": void })
21   -
22   - term = parser {
23   - case f = {ws(Rule.float)}: f
24   - case `(` ~expr `)`: expr
25   - }
26   -
27   - factor = parser {
28   - case ~term "*" ~factor : term * factor
29   - case ~term "/" ~factor : term / factor
30   - case ~term : term
31   - }
32   -
33   - expr = parser {
34   - case ~factor "+" ~expr : factor + expr
35   - case ~factor "-" ~expr : factor - expr
36   - case ~factor : factor
37   - }
38   -
39   - search = ws(parser { case "search" : void })
40   - set = ws(parser { case "set" : void })
41   - next = ws(parser { case "next" : void })
42   - prev = ws(parser { case "prev" : void })
43   - page = ws(parser { case "page" : void })
44   -
45   - args = parser {
46   - case txt=(.*) : List.map(String.trim,String.explode(" ",Text.to_string(txt)))
47   - }
48   -
49   - shell = parser {
50   - case search ~args : { search:args }
51   - case set ~args : { set:args }
52   - case next : { next }
53   - case prev : { prev }
54   - case page pagenum={ws(Rule.natural)} : { ~pagenum }
55   - case command={ws(Rule.ident)} arg={ws(Rule.ident)} : { ~command, ~arg }
56   - case ~expr : {value: expr}
57   - }
58   -
59   - function compute(s) {
60   - match (Parser.try_parse(expr, s)) {
61   - case {some: result}: "{result}";
62   - case {none}: "unknown";
63   - }
64   - }
65   -}
165 src/search.opa
@@ -14,6 +14,36 @@ type search_params = {
14 14
15 15 database string /blekko_auth_key
16 16
  17 +module SearchParser {
  18 +
  19 + function int_of_string(string str) {
  20 + (option(int)) Parser.try_parse(Rule.integer, str);
  21 + }
  22 +
  23 + function nat_of_string(string str) {
  24 + (option(int)) Parser.try_parse(Rule.natural, str);
  25 + }
  26 +
  27 + search = Calc.ws(parser { case "search" : void })
  28 + set = Calc.ws(parser { case "set" : void })
  29 + next = Calc.ws(parser { case "next" : void })
  30 + prev = Calc.ws(parser { case "prev" : void })
  31 + page = Calc.ws(parser { case "page" : void })
  32 +
  33 + args = parser {
  34 + case txt=(.*) : List.map(String.trim,String.explode(" ",Text.to_string(txt)))
  35 + }
  36 +
  37 + shell = parser {
  38 + case search ~args : Search.search(args)
  39 + case set ~args : Search.set(args)
  40 + case next : Search.next()
  41 + case prev : Search.prev()
  42 + case page pagenum={Calc.ws(Rule.natural)} : Search.page(pagenum)
  43 + }
  44 +
  45 +}
  46 +
17 47 module Search {
18 48
19 49 auth =
@@ -117,7 +147,7 @@ Please re-run your application with: --blekko-config option")
117 147 match (args) {
118 148 case ["auth",auth]: set_auth({some:auth}); <>set auth to {auth}</>
119 149 case ["ps",ps]:
120   - match (Calc.nat_of_string(ps)) {
  150 + match (SearchParser.nat_of_string(ps)) {
121 151 case {some:ps}:
122 152 set_ps({some:ps}); <>set ps to {ps}</>
123 153 case {none}: <>set ps &lt;int&gt;</>
@@ -282,122 +312,21 @@ Please re-run your application with: --blekko-config option")
282 312 <>{s}</>
283 313 }
284 314
  315 + Service.spec spec =
  316 + { initial_state: void,
  317 + metadata: {
  318 + id: "search",
  319 + description: "Performing web search with Blekko",
  320 + cmds: [ { cmd: "search [terms]", description: "Performs a web search with given keywords" },
  321 + { cmd: "next", description: "Shows next page with results" },
  322 + { cmd: "prev", description: "Shows previous page with results" },
  323 + { cmd: "page [num]", description: "Shows 'num' page with results" } ],
  324 + },
  325 + function parse_cmd(_) {
  326 + parser {
  327 + case res=SearchParser.shell: Service.respond_with(res)
  328 + }
  329 + }
  330 + }
285 331 }
286 332
287   -/*
288   -xmlns:{args = [{name = version; namespace = ; value = 2.0}]; content = [{args = []; content = [{args = []; content = [{text = blekko | rss for &quot;cooking /findslashtag /rss /ps=2 /p=1&quot;}]; namespace = ; specific_attributes = {none = {}}; tag = title}, {args = []; content = [{text = http:/blekko.com/?q=cooking+%2Ffindslashtag+%2Frss+%2Fps%3D2+%2Fp%3D1}]; namespace = ; specific_attributes = {none = {}}; tag = link}, {args = []; content = [{text = Blekko search for &quot;cooking /findslashtag /rss /ps=2 /p=1&quot;}]; namespace = ; specific_attributes = {none = {}}; tag = description}, {args = []; content = [{text = en-us}]; namespace = ; specific_attributes = {none = {}}; tag = language}, {args = []; content = [{text = Copyright 2011 Blekko, Inc.}]; namespace = ; specific_attributes = {none = {}}; tag = copyright}, {args = []; content = [{text = http:/cyber.law.harvard.edu/rss/rss.html}]; namespace = ; specific_attributes = {none = {}}; tag = docs}, {args = []; content = [{text = webmaster@blekko.com}]; namespace = ; specific_attributes = {none = {}}; tag = webMaster}, {args = []; content = [{text = 265}]; namespace = ; specific_attributes = {none = {}}; tag = rescount}, {args = []; content = [{args = []; content = [{text = /tom/reviews}]; namespace = ; specific_attributes = {none = {}}; tag = title}, {args = []; content = [{text = http:/blekko.com/ws/view+/tom/reviews}]; namespace = ; specific_attributes = {none = {}}; tag = link}, {args = []; content = [{text = http:/blekko.com/ws/view+/tom/reviews}]; namespace = ; specific_attributes = {none = {}}; tag = guid}, {args = []; content = [{text = comma separated terms}]; namespace = ; specific_attributes = {none = {}}; tag= description}]; namespace = ; specific_attributes = {none = {}}; tag = item}, {args = []; content = [{args = []; content = [{text = /coenvalk/food}]; namespace = ; specific_attributes = {none = {}}; tag = title}, {args = []; content = [{text = http:/blekko.com/ws/view+/coenvalk/food}]; namespace = ; specific_attributes = {none = {}}; tag = link}, {args = []; content = [{text = http:/blekko.com/ws/view+/coenvalk/food}]; namespace = ; specific_attributes = {none = {}}; tag = guid}, {args = []; content = [{text = food, eating, cook, bake}]; namespace = ; specific_attributes = {none = {}}; tag = description}]; namespace = ; specific_attributes = {none = {}}; tag = item}]; namespace = ; specific_attributes = {none = {}}; tag = channel}]; namespace = ; specific_attributes = {none = {}}; tag = rss}
289   -*/
290   -
291   -/*
292   -{args = [{name = version; namespace = ; value = 2.0}];
293   - content = [{args = [];
294   - content = [{args = [];
295   - content = [{text = blekko | rss for &quot;/news helicopter /rss /ps=2&quot;}];
296   - namespace = ;
297   - specific_attributes = {none = {}};
298   - tag = title},
299   - {args = [];
300   - content = [{text = http:/blekko.com/?q=%2Fnews+helicopter+%2Frss+%2Fps%3D2}];
301   - namespace = ;
302   - specific_attributes = {none = {}};
303   - tag = link},
304   - {args = [];
305   - content = [{text = Blekko search for &quot;/news helicopter /rss /ps=2&quot;}];
306   - namespace = ;
307   - specific_attributes = {none = {}};
308   - tag = description},
309   - {args = [];
310   - content = [{text = en-us}];
311   - namespace = ;
312   - specific_attributes = {none = {}};
313   - tag = language},
314   - {args = [];
315   - content = [{text = Copyright 2011 Blekko, Inc.}];
316   - namespace = ;
317   - specific_attributes = {none = {}};
318   - tag = copyright},
319   - {args = [];
320   - content = [{text = http:/cyber.law.harvard.edu/rss/rss.html}];
321   - namespace = ;
322   - specific_attributes = {none = {}};
323   - tag = docs},
324   - {args = [];
325   - content = [{text = webmaster@blekko.com}];
326   - namespace = ;
327   - specific_attributes = {none = {}};
328   - tag = webMaster},
329   - {args = [];
330   - content = [{text = 20K}];
331   - namespace = ;
332   - specific_attributes = {none = {}};
333   - tag = rescount},
334   - {args = [];
335   - content = [{args = [];
336   - content = [{text = Trial begins in civil suit against Robinson Helicopter for 2006 crash - The Daily Breeze}];
337   - namespace = ;
338   - specific_attributes = {none = {}};
339   - tag = title},
340   - {args = [];
341   - content = [{text = http:/www.dailybreeze.com/news/ci_19822166?source=rss}];
342   - namespace = ;
343   - specific_attributes = {none = {}};
344   - tag = link},
345   - {args = [];
346   - content = [{text = "http:/www.dailybreeze.com/news/ci_19822166?source=rss Wed, 25 Jan 2012 20:38:43 -0800"}];
347   - namespace = ;
348   - specific_attributes = {none = {}};
349   - tag = guid},
350   - {args = [];
351   - content = [{text = But according to Robinson Helicopter&amp;
352   - #39;
353   - s Raymond Hane, the likely cause of the accident was that Verellen entrusted the controls to Straatman, who did not have a pilot&amp;
354   - #39;
355   - s license. He said a Robinson test pilot checked out. The chopper before the keys were turned over to Verellen.}];
356   - namespace = ;
357   - specific_attributes = {none = {}};
358   - tag = description},
359   - {args = [];
360   - content = [{text = Wed, 25 Jan 2012 20:38:43 -0800}];
361   - namespace = ;
362   - specific_attributes = {none = {}};
363   - tag = pubDate}];
364   - namespace = ;
365   - specific_attributes = {none = {}};
366   - tag = item},
367   - {args = [];
368   - content = [{args = [];
369   - content = [{text = Eurocopter Eyes Brazil Helicopter Exports By 2025 - Gannett Government Media - defensenews.com}];
370   - namespace = ;
371   - specific_attributes = {none = {}};
372   - tag = title},
373   - {args = [];
374   - content = [{text = http:/www.defensenews.com/article/20120125/DEFREG01/301250013/Eurocopter-Eyes-Brazil-Helicopter-Exports-By-2025?odyssey=tab%7Ctopnews%7Ctext%7CFRONTPAGE}];
375   - namespace = ;
376   - specific_attributes = {none = {}};
377   - tag = link},
378   - {args = [];
379   - content = [{text = http:/www.defensenews.com/article/20120125/DEFREG01/301250013/Eurocopter-Eyes-Brazil-Helicopter-Exports-By-2025?odyssey=tab%7Ctopnews%7Ctext%7CFRONTPAGE Wed, 25 Jan 2012 16:14:12 -0800}];
380   - namespace = ;
381   - specific_attributes = {none = {}};
382   - tag = guid},
383   - {args = [];
384   - content =[{text = Currently, Eurocopter helicopters are designed in Europe. A Brazilian factory in Itajuba, Minas Gerais, only assembles Ecureuils helicopters.}];
385   - namespace = ;
386   - specific_attributes = {none = {}};
387   - tag = description},
388   - {args = [];
389   - content = [{text = Wed, 25 Jan 2012 16:14:12 -0800}];
390   - namespace = ;
391   - specific_attributes = {none = {}};
392   - tag = pubDate}];
393   - namespace = ;
394   - specific_attributes = {none = {}};
395   - tag = item}];
396   - namespace = ;
397   - specific_attributes = {none = {}};
398   - tag = channel}];
399   - namespace = ;
400   - specific_attributes = {none = {}};
401   - tag = rss}
402   -*/
403   -
185 src/service.opa
... ... @@ -0,0 +1,185 @@
  1 +// license: AGPL
  2 +// (c) MLstate, 2011, 2012
  3 +// author: Adam Koprowski
  4 +
  5 +type Service.response =
  6 + { xhtml outcome } or { string redirect }
  7 +
  8 +type Service.state_change('state) =
  9 + { no_change } or { 'state new_state }
  10 +
  11 + // service response to a command
  12 +type Service.outcome('state) =
  13 + { Service.response response, Service.state_change('state) state_change }
  14 +
  15 +// meta-data about the service (for help)
  16 +type Service.metadata =
  17 + { string id,
  18 + string description,
  19 + list({string cmd, string description}) cmds
  20 + }
  21 +
  22 + // specification of a single service
  23 +type Service.spec('state) =
  24 + { 'state initial_state,
  25 + ('state -> Parser.general_parser(Service.outcome('state))) parse_cmd,
  26 + Service.metadata metadata
  27 + }
  28 +
  29 +type Service.cmd_executor =
  30 + string -> {cannot_handle} or {Service.response response}
  31 +
  32 +type Service.fun_executor('state) =
  33 + ('state -> 'state) -> void
  34 +
  35 +type Service.handler =
  36 + { Service.cmd_executor cmd_executor,
  37 + Service.metadata metadata
  38 + }
  39 +
  40 + // implementation of a service
  41 +type Service.t('state) =
  42 + { Service.fun_executor('state) fun_executor,
  43 + Service.handler handler
  44 + }
  45 +
  46 +server module Service {
  47 +
  48 + private function execute_cmd(service, state, cmd) {
  49 + cmd_parser = service.spec.parse_cmd(state)
  50 + match (Parser.try_parse(cmd_parser, cmd)) {
  51 + case {some: res}:
  52 + instruction =
  53 + match (res.state_change) {
  54 + case {no_change}: {unchanged}
  55 + case {new_state: state}: {set: state}
  56 + }
  57 + { return: {response: res.response}, ~instruction }
  58 + default:
  59 + { return: {cannot_handle},
  60 + instruction: {unchanged}
  61 + }
  62 + }
  63 + }
  64 +
  65 + private function execute_fun(state, fun) {
  66 + new_state = fun(state)
  67 + { return: {cannot_handle},
  68 + instruction: {set: new_state}
  69 + }
  70 + }
  71 +
  72 + private function process_request(service, state, cmd) {
  73 + match (cmd) {
  74 + case {execute_fun: fun}: execute_fun(state, fun)
  75 + case {execute_cmd: cmd}: execute_cmd(service, state, cmd)
  76 + }
  77 + }
  78 +
  79 + // builds a service from its specification
  80 + function Service.t make({Service.spec spec, ...} service) {
  81 + cell = Cell.make(service.spec.initial_state, process_request(service, _, _))
  82 + { fun_executor: function (fun) { _ = Cell.call(cell, {execute_fun: fun}); void },
  83 + handler: {
  84 + cmd_executor: function (cmd) { Cell.call(cell, {execute_cmd: cmd}) },
  85 + metadata: service.spec.metadata
  86 + }
  87 + }
  88 + }
  89 +
  90 + function respond_with(xhtml) {
  91 + { state_change: {no_change},
  92 + response: {outcome: xhtml}
  93 + }
  94 + }
  95 +
  96 +}
  97 +
  98 +// implementation of a system (consisting of a bunch of services)
  99 +abstract type System.t = list(Service.handler)
  100 +
  101 +server module Shell {
  102 +
  103 + function System.t build(list(Service.handler) services) {
  104 + services
  105 + }
  106 +
  107 + function xhtml execute(System.t sys, string cmd) {
  108 + recursive function aux(services) {
  109 + match (services) {
  110 + case []:
  111 + <>Unknown command</>
  112 + case [service | services]:
  113 + match (service.cmd_executor(cmd)) {
  114 + case {response: {~outcome}}:
  115 + outcome
  116 + case {response: {~redirect}}:
  117 + Client.goto(redirect);
  118 + <></>
  119 + case {cannot_handle}:
  120 + aux(services)
  121 + }
  122 + }
  123 + }
  124 + aux([core_handler(sys) | sys])
  125 + }
  126 +
  127 + private function print_generic_help(mods) {
  128 + function present_module(mod) {
  129 + <li><strong>{mod.id}</>: {mod.description}</>
  130 + }
  131 + <>Use: '<strong>help module</>' to get help about using given module. Available modules:
  132 + <ul>{List.map(present_module, mods)}</>
  133 + </>
  134 + }
  135 +
  136 + private function print_help_for(mods, mod_name) {
  137 + function show_cmd(cmd) {
  138 + <li><strong>{cmd.cmd}</>: {cmd.description}</>
  139 + }
  140 + function show_help(mod) {
  141 + <>
  142 + <strong>{mod.id}</>: {mod.description}
  143 + <ul>{List.map(show_cmd, mod.cmds)}</>
  144 + </>
  145 + }
  146 + function good_module(m) { m.id == mod_name }
  147 + match (List.find(good_module, mods)) {
  148 + case {some: mod}: show_help(mod)
  149 + default:
  150 + all_modules = List.map(_.id, mods)
  151 + <>Unknown module: '{mod_name}'. Available modules: {List.to_string_using("", "", ", ", all_modules)}</>
  152 + }
  153 + }
  154 +
  155 + private function core_parser(mods) {
  156 + parser {
  157 + case "help": print_generic_help(mods)
  158 + case "help" Rule.ws mod=(.*): print_help_for(mods, Text.to_string(mod))
  159 + case "clear":
  160 + LineEditor.clear();
  161 + #terminal_prev = <></>;
  162 + <></>
  163 + }
  164 + }
  165 +
  166 + private function core_executor(mods)(string cmd) {
  167 + match (Parser.try_parse(core_parser(mods), cmd)) {
  168 + case {some: res}: {response: {outcome: res}}
  169 + default: {cannot_handle}
  170 + }
  171 + }
  172 +
  173 + private function core_handler(mods) {
  174 + metadata =
  175 + { id: "core",
  176 + description: "Core shell functionality",
  177 + cmds: [ { cmd: "clear", description: "Clear the shell screen" },
  178 + { cmd: "help [module]", description: "Prints help about given 'module'. If module ommited prints all available modules." } ]
  179 + }
  180 + { cmd_executor: core_executor([metadata | List.map(_.metadata, mods)]),
  181 + ~metadata
  182 + }
  183 + }
  184 +
  185 +}
120 src/twitter.opa
... ... @@ -0,0 +1,120 @@
  1 +// license: AGPL
  2 +// (c) MLstate, 2011, 2012
  3 +// author: Adam Koprowski
  4 +
  5 +import stdlib.apis.{twitter, oauth}
  6 +
  7 +database Twitter.configuration /twitter_config
  8 +
  9 +type Twitter.status = {no_credentials}
  10 + or {string request_secret, string request_token}
  11 + or {Twitter.credentials authenticated}
  12 +
  13 +module TwitterConnect
  14 +{
  15 +
  16 + server config =
  17 + _ = CommandLine.filter(
  18 + { init: void
  19 + , parsers: [{ CommandLine.default_parser with
  20 + names: ["--twitter-config"],
  21 + param_doc: "APP_KEY,APP_SECRET",
  22 + description: "Sets the application data for the associated Twitter application",
  23 + function on_param(state) {
  24 + parser {
  25 + case app_key=Rule.alphanum_string [,] app_secret=Rule.alphanum_string:
  26 + {
  27 + /twitter_config <- { consumer_key: app_key,
  28 + consumer_secret: app_secret
  29 + }
  30 + {no_params: state}
  31 + }
  32 + }
  33 + }
  34 + }]
  35 + , anonymous: []
  36 + , title: "Twitter configuration"
  37 + }
  38 + )
  39 + match (?/twitter_config) {
  40 + case {some: config}: config
  41 + default:
  42 + Log.error("webshell[config]", "Cannot read Twitter configuration (application key and/or secret key)
  43 +Please re-run your application with: --twitter-config option")
  44 + System.exit(1)
  45 + }
  46 +
  47 + private TW = Twitter(config)
  48 + private TWA = OAuth(TW.oauth_params({fast}))
  49 +
  50 + private redirect = "http://{Config.host}/connect/twitter"
  51 +
  52 + function login(executor)(raw_token) {
  53 + function connect(auth_data) {
  54 + Log.info("Twitter", "connection data: {raw_token}")
  55 + authentication_failed = {no_credentials}
  56 + match (auth_data) {
  57 + case ~{request_secret, request_token}:
  58 + match (TWA.connection_result(raw_token)) {
  59 + case {success: s}:
  60 + if (s.token == request_token) {
  61 + match (TWA.get_access_token(s.token, request_secret, s.verifier)) {
  62 + case {success: s}:
  63 + twitter_creds = {access_token: s.token, access_secret: s.secret}
  64 + Log.info("Twitter", "got credentials: {twitter_creds}")
  65 + {authenticated: twitter_creds}
  66 + default:
  67 + authentication_failed
  68 + }
  69 + } else
  70 + authentication_failed
  71 + default:
  72 + authentication_failed
  73 + }
  74 + default:
  75 + authentication_failed
  76 + }
  77 + }
  78 + executor(connect)
  79 + }
  80 +
  81 + private function authenticate() {
  82 + match (TWA.get_request_token(redirect)) {
  83 + case {~error}:
  84 + Service.respond_with(<>Twitter authorization failed</>)
  85 + case {success: token}:
  86 + auth_url = TWA.build_authorize_url(token.token)
  87 + auth_state = {request_secret: token.secret, request_token: token.token}
  88 + { response: {redirect: auth_url},
  89 + state_change: {new_state: auth_state}
  90 + }
  91 + }
  92 + }
  93 +
  94 + private function tweet(state, content) {
  95 + match (state) {
  96 + case {authenticated: creds}:
  97 + tweet = TW.post_status(content, "", creds)
  98 + Service.respond_with(<>Tweeted: <em>«{tweet.text}»</></>)
  99 + default:
  100 + authenticate()
  101 + }
  102 + }
  103 +
  104 + Service.spec spec =
  105 + { initial_state: Twitter.status {no_credentials},
  106 + metadata: {
  107 + id: "twitter",
  108 + description: "Managing Twitter account",
  109 + cmds: [
  110 + { cmd: "tweet [content]", description: "Publishes a given tweet" }
  111 + ],
  112 + },
  113 + function parse_cmd(state) {
  114 + parser {
  115 + case "tweet" Rule.ws content=(.*) : tweet(state, Text.to_string(content))
  116 + }
  117 + }
  118 + }
  119 +
  120 +}
69 src/webshell.opa
@@ -8,6 +8,14 @@ import stdlib.widgets.bootstrap
8 8
9 9 WB = WBootstrap
10 10
  11 +calc = Service.make(Calc)
  12 +search = Service.make(Search)
  13 +dropbox = Service.make(DropboxConnect)
  14 +twitter = Service.make(TwitterConnect)
  15 +facebook = Service.make(FacebookConnect)
  16 +
  17 +shell = Shell.build([calc.handler, search.handler, dropbox.handler, twitter.handler, facebook.handler])
  18 +
11 19 function focus(set) {
12 20 Log.warning("focus", set);
13 21 #status = "Focus: {set}";
@@ -33,26 +41,13 @@ function loop(ua)(_) {
33 41 LineEditor.init(ua, #editor, readevalwrite(_), true);
34 42 }
35 43
36   -function answer(expr) {
37   - (xhtml) match (Parser.try_parse(Calc.shell, expr)) {
38   - case { none }: <>syntax error</>
39   - case { some: { value: result } }: <>{result}</>
40   - case { some: { ~command, ~arg } }: <>{command}({arg})</>
41   - case { some: { search:args } }: Search.search(args)
42   - case { some: { set:args } }: Search.set(args)
43   - case { some: { next } }: Search.next()
44   - case { some: { prev } }: Search.prev()
45   - case { some: { ~pagenum } }: Search.page(pagenum)
46   - }
47   -}
48   -
49   -function readevalwrite(expr) {
  44 +function readevalwrite(cmd) {
50 45 element =
51 46 <div>
52 47 <span>{prompt()}</span>
53   - <span>{expr}</span>
  48 + <span>{cmd}</span>
54 49 </div>
55   - <div>{answer(expr)}</div>;
  50 + <div>{Shell.execute(shell, cmd)}</div>;
56 51 update(element);
57 52 }
58 53
@@ -67,9 +62,7 @@ function login_box() {
67 62 function block(content) {
68 63 <h3 style="float: right">{content}</>
69 64 }
70   - login =
71   - prompt = <a>You can sign in with:</>
72   - block(<>{prompt}{FacebookConnect.xhtml}{DropboxConnect.xhtml}</>)
  65 + login = <>{FacebookConnect.xhtml}</>
73 66 logout =
74 67 function do_logout(_) {
75 68 Login.set_current_user({guest})
@@ -78,19 +71,22 @@ function login_box() {
78 71 name = <a>{Login.get_current_user_name()}</>
79 72 button = WBootstrap.Button.make({ button: <>Logout</>, callback: do_logout}, [])
80 73 block(<>{button}{name}</>)
81   - match (Login.get_current_user()) {
82   - case {guest}: login
83   - default: logout
84   - }
  74 + content =
  75 + match (Login.get_current_user()) {
  76 + case {guest}: login
  77 + default: logout
  78 + }
  79 + <span class=userbox>{content}</>
85 80 }
86 81
87   -function page() {
  82 +function page(cmd) {
88 83 topbar =
89   - WB.Navigation.topbar(
  84 + WB.Navigation.fixed_navbar(
90 85 WB.Layout.fixed(
91 86 WB.Navigation.brand(<>webshell</>, none, ignore) <+>
92 87 {login_box()}
93   - )
  88 + ),
  89 + {top}
94 90 )
95 91 html = WB.Layout.fixed(
96 92 <div id="terminal">
@@ -101,10 +97,16 @@ function page() {
101 97 </>
102 98 </>
103 99 )
  100 + function onready(_) {
  101 + match (cmd) {
  102 + case {some: cmd}: readevalwrite(cmd)
  103 + default: void
  104 + }
  105 + }
104 106 Resource.html("webshell",
105 107 <>
106 108 {topbar}
107   - {html}
  109 + <span onready={onready}>{html}</>
108 110 <div id="status"/>
109 111 </>
110 112 )
@@ -116,9 +118,16 @@ function connect(connector, raw_data) {
116 118 }
117 119
118 120 dispatcher = parser {
119   - case "/connect/facebook?" data=(.*) : connect(FacebookConnect.login, data)
120   - case "/connect/dropbox?" data=(.*) : connect(DropboxConnect.connect, data)
121   - case .* : page()
  121 + case "/connect/facebook?" data=(.*) ->
  122 + connect(FacebookConnect.login(facebook.fun_executor), data)
  123 + case "/connect/twitter?" data=(.*) ->
  124 + connect(TwitterConnect.login(twitter.fun_executor), data)
  125 + case "/connect/dropbox?" data=(.*) ->
  126 + connect(DropboxConnect.login(dropbox.fun_executor), data)
  127 + case "/do=" cmd=(.*) ->
  128 + page(some(Text.to_string(cmd)))
  129 + case .* ->
  130 + page(none)
122 131 }
123 132
124 133 Server.start(Server.http,

No commit comments for this range

Something went wrong with that request. Please try again.