Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39537
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
DEBUG = false
23
24
# Maximum number of outstanding concurrent messages (that have responses)
25
# to send at once to the backend.
26
MAX_CONCURRENT = 25
27
28
{EventEmitter} = require('events')
29
30
async = require('async')
31
_ = require('underscore')
32
33
syncstring = require('./syncstring')
34
synctable = require('./synctable')
35
db_doc = require('./db-doc')
36
37
smc_version = require('./smc-version')
38
39
message = require("./message")
40
misc = require("./misc")
41
42
{validate_client_query} = require('./schema-validate')
43
44
defaults = misc.defaults
45
required = defaults.required
46
47
# JSON_CHANNEL is the channel used for JSON. The hub imports this
48
# file, so if this constant is ever changed (for some reason?), it
49
# only has to be changed on this one line. Moreover, channel
50
# assignment in the hub is implemented *without* the assumption that
51
# the JSON channel is '\u0000'.
52
JSON_CHANNEL = '\u0000'
53
exports.JSON_CHANNEL = JSON_CHANNEL # export, so can be used by hub
54
55
# Default timeout for many operations -- a user will get an error in many cases
56
# if there is no response to an operation after this amount of time.
57
DEFAULT_TIMEOUT = 30 # in seconds
58
59
60
class Session extends EventEmitter
61
# events:
62
# - 'open' -- session is initialized, open and ready to be used
63
# - 'close' -- session's connection is closed/terminated
64
# - 'execute_javascript' -- code that server wants client to run related to this session
65
constructor: (opts) ->
66
opts = defaults opts,
67
conn : required # a Connection instance
68
project_id : required
69
session_uuid : required
70
params : undefined
71
data_channel : undefined # optional extra channel that is used for raw data
72
init_history : undefined # used for console
73
74
@start_time = misc.walltime()
75
@conn = opts.conn
76
@params = opts.params
77
if @type() == 'console'
78
if not @params?.path? or not @params?.filename?
79
throw Error("params must be specified with path and filename")
80
@project_id = opts.project_id
81
@session_uuid = opts.session_uuid
82
@data_channel = opts.data_channel
83
@init_history = opts.init_history
84
@emit("open")
85
86
## This is no longer necessary; or rather, it's better to only
87
## reset terminals, etc., when they are used, since it wastes
88
## less resources.
89
# I'm going to leave this in for now -- it's only used for console sessions,
90
# and they aren't properly reconnecting in all cases.
91
if @reconnect?
92
@conn.on("connected", @reconnect)
93
94
close: () =>
95
@removeAllListeners()
96
if @reconnect?
97
@conn.removeListener("connected", @reconnect)
98
99
reconnect: (cb) =>
100
# Called when the connection gets dropped, then reconnects
101
if not @conn._signed_in
102
setTimeout((()=>@reconnect(cb)), 500)
103
return
104
105
if @_reconnect_lock
106
#console.warn('reconnect: lock')
107
cb?("reconnect: hit lock")
108
return
109
110
@emit "reconnecting"
111
@_reconnect_lock = true
112
#console.log("reconnect: #{@type()} session with id #{@session_uuid}...")
113
f = (cb) =>
114
@conn.call
115
message : message.connect_to_session
116
session_uuid : @session_uuid
117
type : @type()
118
project_id : @project_id
119
params : @params
120
timeout : 30
121
cb : (err, reply) =>
122
if err
123
cb(err); return
124
switch reply.event
125
when 'error'
126
cb(reply.error)
127
when 'session_connected'
128
#console.log("reconnect: #{@type()} session with id #{@session_uuid} -- SUCCESS", reply)
129
if @data_channel != reply.data_channel
130
@conn.change_data_channel
131
prev_channel : @data_channel
132
new_channel : reply.data_channel
133
session : @
134
@data_channel = reply.data_channel
135
@init_history = reply.history
136
cb()
137
else
138
cb("bug in hub")
139
misc.retry_until_success
140
max_time : 15000
141
factor : 1.3
142
f : f
143
cb : (err) =>
144
#console.log("reconnect('#{@session_uuid}'): finished #{err}")
145
delete @_reconnect_lock
146
if not err
147
@emit("reconnect")
148
cb?(err)
149
150
terminate_session: (cb) =>
151
@conn.call
152
message :
153
message.terminate_session
154
project_id : @project_id
155
session_uuid : @session_uuid
156
timeout : 30
157
cb : cb
158
159
walltime: () =>
160
return misc.walltime() - @start_time
161
162
handle_data: (data) =>
163
@emit("data", data)
164
165
write_data: (data) ->
166
@conn.write_data(@data_channel, data)
167
168
restart: (cb) =>
169
@conn.call(message:message.restart_session(session_uuid:@session_uuid), timeout:10, cb:cb)
170
171
172
###
173
#
174
# A Console session, which connects the client to a pty on a remote machine.
175
#
176
# Client <-- (primus) ---> Hub <--- (tcp) ---> console_server
177
#
178
###
179
180
class ConsoleSession extends Session
181
type: () => "console"
182
183
184
185
186
class exports.Connection extends EventEmitter
187
# Connection events:
188
# - 'connecting' -- trying to establish a connection
189
# - 'connected' -- succesfully established a connection; data is the protocol as a string
190
# - 'error' -- called when an error occurs
191
# - 'output' -- received some output for stateless execution (not in any session)
192
# - 'execute_javascript' -- code that server wants client to run (not for a particular session)
193
# - 'message' -- emitted when a JSON message is received on('message', (obj) -> ...)
194
# - 'data' -- emitted when raw data (not JSON) is received -- on('data, (id, data) -> )...
195
# - 'signed_in' -- server pushes a succesful sign in to the client (e.g., due to
196
# 'remember me' functionality); data is the signed_in message.
197
# - 'project_list_updated' -- sent whenever the list of projects owned by this user
198
# changed; data is empty -- browser could ignore this unless
199
# the project list is currently being displayed.
200
# - 'project_data_changed - sent when data about a specific project has changed,
201
# e.g., title/description/settings/etc.
202
# - 'new_version', number -- sent when there is a new version of the source code so client should refresh
203
204
constructor: (@url) ->
205
# Tweaks the maximum number of listeners an EventEmitter can have -- 0 would mean unlimited
206
# The issue is https://github.com/sagemathinc/cocalc/issues/1098 and the errors we got are
207
# (node) warning: possible EventEmitter memory leak detected. 301 listeners added. Use emitter.setMaxListeners() to increase limit.
208
@setMaxListeners(3000) # every open file/table/sync db listens for connect event, which adds up.
209
210
@emit("connecting")
211
@_call =
212
queue : [] # messages in the queue to send
213
count : 0 # number of message currently outstanding
214
@_id_counter = 0
215
@_sessions = {}
216
@_new_sessions = {}
217
@_data_handlers = {}
218
@execute_callbacks = {}
219
@call_callbacks = {}
220
@_project_title_cache = {}
221
@_usernames_cache = {}
222
@_redux = undefined # set this if you want to be able to use mark_file
223
224
@register_data_handler(JSON_CHANNEL, @handle_json_data)
225
226
@on 'connected', @send_version
227
228
# IMPORTANT! Connection is an abstract base class. Derived classes must
229
# implement a method called _connect that takes a URL and a callback, and connects to
230
# the Primus websocket server with that url, then creates the following event emitters:
231
# "connected", "error", "close"
232
# and returns a function to write raw data to the socket.
233
@_connect @url, (data) =>
234
if data.length > 0 # all messages must start with a channel; length 0 means nothing.
235
# Incoming messages are tagged with a single UTF-16
236
# character c (there are 65536 possibilities). If
237
# that character is JSON_CHANNEL, the message is
238
# encoded as JSON and we handle it in the usual way.
239
# If the character is anything else, the raw data in
240
# the message is sent to an appropriate handler, if
241
# one has previously been registered. The motivation
242
# is that we the ability to multiplex multiple
243
# sessions over a *single* WebSocket connection, and it
244
# is absolutely critical that there is minimal
245
# overhead regarding the amount of data transfered --
246
# 1 character is minimal!
247
248
channel = data[0]
249
data = data.slice(1)
250
251
@_handle_data(channel, data)
252
253
# give other listeners a chance to do something with this data.
254
@emit("data", channel, data)
255
@_connected = false
256
257
# start pinging -- not used/needed for primus, but *is* needed for getting information about server_time
258
# In particular, this ping time is not reported to the user and is not used as a keep-alive, hence it
259
# can be fairly long.
260
@_ping_interval = 60000
261
@_ping()
262
263
dbg: (f) =>
264
return (m...) ->
265
switch m.length
266
when 0
267
s = ''
268
when 1
269
s = m[0]
270
else
271
s = JSON.stringify(m)
272
console.log("#{(new Date()).toISOString()} - Client.#{f}: #{s}")
273
274
_ping: () =>
275
@_ping_interval ?= 60000 # frequency to ping
276
@_last_ping = new Date()
277
@call
278
message : message.ping()
279
timeout : 15 # CRITICAL that this timeout be less than the @_ping_interval
280
cb : (err, pong) =>
281
if not err
282
now = new Date()
283
# Only record something if success, got a pong, and the round trip is short!
284
# If user messes with their clock during a ping and we don't do this, then
285
# bad things will happen.
286
if pong?.event == 'pong' and now - @_last_ping <= 1000*15
287
@_last_pong = {server:pong.now, local:now}
288
# See the function server_time below; subtract @_clock_skew from local time to get a better
289
# estimate for server time.
290
@_clock_skew = @_last_ping - 0 + ((@_last_pong.local - @_last_ping)/2) - @_last_pong.server
291
misc.set_local_storage('clock_skew', @_clock_skew)
292
# try again later
293
setTimeout(@_ping, @_ping_interval)
294
295
# Returns (approximate) time in ms since epoch on the server.
296
# NOTE:
297
# This is guaranteed to be an *increasing* function, with an arbitrary
298
# ms added on in case of multiple calls at once, to guarantee uniqueness.
299
# Also, if the user changes their clock back a little, this will still
300
# increase... very slowly until things catch up. This avoids any
301
# possibility of weird random re-ordering of patches within a given session.
302
server_time: =>
303
t = @_server_time()
304
last = @_last_server_time
305
if last? and last >= t
306
# That's annoying -- time is not marching forward... let's fake it until it does.
307
t = new Date((last - 0) + 1)
308
@_last_server_time = t
309
return t
310
311
_server_time: =>
312
# Add _clock_skew to our local time to get a better estimate of the actual time on the server.
313
# This can help compensate in case the user's clock is wildly wrong, e.g., by several minutes,
314
# or even hours due to totally wrong time (e.g. ignoring time zone), which is relevant for
315
# some algorithms including sync which uses time. Getting the clock right up to a small multiple
316
# of ping times is fine for our application.
317
if not @_clock_skew?
318
x = misc.get_local_storage('clock_skew')
319
if x?
320
@_clock_skew = parseFloat(x)
321
if @_clock_skew?
322
return new Date(new Date() - @_clock_skew)
323
else
324
return new Date()
325
326
ping_test: (opts) =>
327
opts = defaults opts,
328
packets : 20
329
timeout : 5 # any ping that takes this long in seconds is considered a fail
330
delay_ms : 200 # wait this long between doing pings
331
log : undefined # if set, use this to log output
332
cb : undefined # cb(err, ping_times)
333
334
###
335
Use like this in a Sage Worksheet:
336
337
%coffeescript
338
s = require('webapp_client').webapp_client
339
s.ping_test(delay_ms:100, packets:40, log:print)
340
###
341
ping_times = []
342
do_ping = (i, cb) =>
343
t = new Date()
344
@call
345
message : message.ping()
346
timeout : opts.timeout
347
cb : (err, pong) =>
348
heading = "#{i}/#{opts.packets}: "
349
if not err and pong?.event == 'pong'
350
ping_time = new Date() - t
351
bar = ('*' for j in [0...Math.floor(ping_time/10)]).join('')
352
mesg = "#{heading}time=#{ping_time}ms"
353
else
354
bar = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
355
mesg = "#{heading}Request error -- #{err}, #{misc.to_json(pong)}"
356
ping_time = Infinity
357
while mesg.length < 40
358
mesg += ' '
359
mesg += bar
360
if opts.log?
361
opts.log(mesg)
362
else
363
console.log(mesg)
364
ping_times.push(ping_time)
365
setTimeout(cb, opts.delay_ms)
366
async.mapSeries([1..opts.packets], do_ping, (err) => opts.cb?(err, ping_times))
367
368
369
close: () =>
370
@_conn.close() # TODO: this looks very dubious -- probably broken or not used anymore
371
372
version: =>
373
return smc_version.version
374
375
send_version: =>
376
@send(message.version(version:@version()))
377
378
# Send a JSON message to the hub server.
379
send: (mesg) =>
380
#console.log("send at #{misc.mswalltime()}", mesg)
381
@write_data(JSON_CHANNEL, misc.to_json_socket(mesg))
382
383
# Send raw data via certain channel to the hub server.
384
write_data: (channel, data) =>
385
try
386
@_write(channel + data)
387
catch err
388
# TODO: this happens when trying to send and the client not connected
389
# We might save up messages in a local queue and keep retrying, for
390
# a sort of offline mode ? I have not worked out how to handle this yet.
391
#console.log(err)
392
393
is_signed_in: =>
394
return @is_connected() and !!@_signed_in
395
396
# account_id or project_id of this client
397
client_id: () =>
398
return @account_id
399
400
# false since this client is not a project
401
is_project: () =>
402
return false
403
404
# true since this client is a user
405
is_user: () =>
406
return true
407
408
is_connected: => !!@_connected
409
410
remember_me_key: => "remember_me#{window?.app_base_url ? ''}"
411
412
handle_json_data: (data) =>
413
mesg = misc.from_json_socket(data)
414
if DEBUG
415
console.log("handle_json_data: #{data}")
416
switch mesg.event
417
when "execute_javascript"
418
if mesg.session_uuid?
419
@_sessions[mesg.session_uuid].emit("execute_javascript", mesg)
420
else
421
@emit("execute_javascript", mesg)
422
when "output"
423
cb = @execute_callbacks[mesg.id]
424
if cb?
425
cb(mesg)
426
delete @execute_callbacks[mesg.id] if mesg.done
427
if mesg.session_uuid? # executing in a persistent session
428
@_sessions[mesg.session_uuid].emit("output", mesg)
429
else # stateless exec
430
@emit("output", mesg)
431
when "terminate_session"
432
session = @_sessions[mesg.session_uuid]
433
session?.emit("close")
434
when "session_reconnect"
435
if mesg.data_channel?
436
@_sessions[mesg.data_channel]?.reconnect()
437
else if mesg.session_uuid?
438
@_sessions[mesg.session_uuid]?.reconnect()
439
when "cookies"
440
@_cookies?(mesg)
441
442
when "signed_in"
443
@account_id = mesg.account_id
444
@_signed_in = true
445
misc.set_local_storage(@remember_me_key(), true)
446
@_sign_in_mesg = mesg
447
@emit("signed_in", mesg)
448
449
when "remember_me_failed"
450
misc.delete_local_storage(@remember_me_key())
451
@emit(mesg.event, mesg)
452
453
when "project_list_updated", 'project_data_changed'
454
@emit(mesg.event, mesg)
455
when 'version'
456
@emit('new_version', {version:mesg.version, min_version:mesg.min_version})
457
when "error"
458
# An error that isn't tagged with an id -- some sort of general problem.
459
if not mesg.id?
460
console.log("WARNING: #{misc.to_json(mesg.error)}")
461
return
462
463
id = mesg.id # the call f(null,mesg) can mutate mesg (!), so we better save the id here.
464
v = @call_callbacks[id]
465
if v?
466
{cb, error_event} = v
467
v.first = false
468
if error_event and mesg.event == 'error'
469
cb(mesg.error)
470
else
471
cb(undefined, mesg)
472
if not mesg.multi_response
473
delete @call_callbacks[id]
474
475
# Finally, give other listeners a chance to do something with this message.
476
@emit('message', mesg)
477
478
change_data_channel: (opts) =>
479
opts = defaults opts,
480
prev_channel : required
481
new_channel : required
482
session : required
483
@unregister_data_handler(opts.prev_channel)
484
delete @_sessions[opts.prev_channel]
485
@_sessions[opts.new_channel] = opts.session
486
@register_data_handler(opts.new_channel, opts.session.handle_data)
487
488
register_data_handler: (channel, h) ->
489
@_data_handlers[channel] = h
490
491
unregister_data_handler: (channel) ->
492
delete @_data_handlers[channel]
493
494
_handle_data: (channel, data) =>
495
#console.log("_handle_data:(#{channel},'#{data}')")
496
f = @_data_handlers[channel]
497
if f?
498
f(data)
499
#else
500
# console.log("Error -- missing channel '#{channel}' for data '#{data}'. @_data_handlers = #{misc.to_json(@_data_handlers)}")
501
502
connect_to_session: (opts) ->
503
opts = defaults opts,
504
type : required
505
session_uuid : required
506
project_id : required
507
timeout : DEFAULT_TIMEOUT
508
params : required # must include {path:?, filename:?}
509
cb : required
510
@call
511
message : message.connect_to_session
512
session_uuid : opts.session_uuid
513
type : opts.type
514
project_id : opts.project_id
515
params : opts.params
516
517
timeout : opts.timeout
518
519
cb : (error, reply) =>
520
if error
521
opts.cb(error); return
522
switch reply.event
523
when 'error'
524
opts.cb(reply.error)
525
when 'session_connected'
526
@_create_session_object
527
type : opts.type
528
project_id : opts.project_id
529
session_uuid : opts.session_uuid
530
data_channel : reply.data_channel
531
init_history : reply.history
532
params : opts.params
533
cb : opts.cb
534
else
535
opts.cb("Unknown event (='#{reply.event}') in response to connect_to_session message.")
536
537
new_session: (opts) ->
538
opts = defaults opts,
539
timeout : DEFAULT_TIMEOUT # how long until give up on getting a new session
540
type : "console" # only "console" supported
541
params : required # must include {path:?, filename:?}
542
project_id : required
543
cb : required # cb(error, session) if error is defined it is a string
544
545
@call
546
message : message.start_session
547
type : opts.type
548
params : opts.params
549
project_id : opts.project_id
550
551
timeout : opts.timeout
552
553
cb : (error, reply) =>
554
if error
555
opts.cb(error)
556
else
557
if reply.event == 'error'
558
opts.cb(reply.error)
559
else if reply.event == "session_started" or reply.event == "session_connected"
560
@_create_session_object
561
type : opts.type
562
project_id : opts.project_id
563
session_uuid : reply.session_uuid
564
data_channel : reply.data_channel
565
params : opts.params
566
cb : opts.cb
567
else
568
opts.cb("Unknown event (='#{reply.event}') in response to start_session message.")
569
570
_create_session_object: (opts) =>
571
opts = defaults opts,
572
type : required # 'console'
573
project_id : required
574
session_uuid : required
575
data_channel : undefined
576
params : undefined
577
init_history : undefined
578
cb : required
579
580
session_opts =
581
conn : @
582
project_id : opts.project_id
583
session_uuid : opts.session_uuid
584
data_channel : opts.data_channel
585
init_history : opts.init_history
586
params : opts.params
587
588
switch opts.type
589
when 'console'
590
session = new ConsoleSession(session_opts)
591
else
592
opts.cb("Unknown session type: '#{opts.type}'")
593
@_sessions[opts.session_uuid] = session
594
if opts.data_channel != JSON_CHANNEL
595
@_sessions[opts.data_channel] = session
596
@register_data_handler(opts.data_channel, session.handle_data)
597
opts.cb(false, session)
598
599
_do_call: (opts, cb) =>
600
if not opts.cb?
601
# console.log("no opts.cb", opts.message)
602
# A call to the backend, but where we do not wait for a response.
603
# In order to maintain at least roughly our limit on MAX_CONCURRENT,
604
# we simply pretend that this message takes about 150ms
605
# to complete. This helps space things out so the server can
606
# handle requests properly, instead of just discarding them (be nice
607
# to the backend and it will be nice to you).
608
@send(opts.message)
609
setTimeout(cb, 150)
610
return
611
612
id = opts.message.id ?= misc.uuid()
613
614
@call_callbacks[id] =
615
cb : (args...) =>
616
if cb?
617
cb()
618
cb = undefined
619
opts.cb(args...)
620
error_event : opts.error_event
621
first : true
622
623
@send(opts.message)
624
625
if opts.timeout
626
setTimeout(
627
(() =>
628
if @call_callbacks[id]?.first
629
error = "Timeout after #{opts.timeout} seconds"
630
if cb?
631
cb()
632
cb = undefined
633
opts.cb(error, message.error(id:id, error:error))
634
delete @call_callbacks[id]
635
), opts.timeout*1000
636
)
637
638
call: (opts={}) =>
639
# This function:
640
# * Modifies the message by adding an id attribute with a random uuid value
641
# * Sends the message to the hub
642
# * When message comes back with that id, call the callback and delete it (if cb opts.cb is defined)
643
# The message will not be seen by @handle_message.
644
# * If the timeout is reached before any messages come back, delete the callback and stop listening.
645
# However, if the message later arrives it may still be handled by @handle_message.
646
opts = defaults opts,
647
message : required
648
timeout : undefined
649
error_event : false # if true, turn error events into just a normal err
650
cb : undefined
651
if not @is_connected()
652
opts.cb?('not connected')
653
return
654
@_call.queue.push(opts)
655
@_update_calls()
656
657
_update_calls: =>
658
while @_call.queue.length > 0 and @_call.count <= MAX_CONCURRENT
659
@_process_next_call()
660
661
_process_next_call: =>
662
if @_call.queue.length == 0
663
return
664
@_call.count += 1
665
#console.log('count (call):', @_call.count)
666
@_do_call @_call.queue.shift(), =>
667
@_call.count -= 1
668
#console.log('count (done):', @_call.count)
669
@_update_calls()
670
671
call_local_hub: (opts) =>
672
opts = defaults opts,
673
project_id : required # determines the destination local hub
674
message : required
675
timeout : undefined
676
cb : undefined
677
m = message.local_hub
678
multi_response : false
679
project_id : opts.project_id
680
message : opts.message
681
timeout : opts.timeout
682
if opts.cb?
683
f = (err, resp) =>
684
#console.log("call_local_hub:#{misc.to_json(opts.message)} got back #{misc.to_json(err:err,resp:resp)}")
685
opts.cb?(err, resp)
686
else
687
f = undefined
688
689
@call
690
message : m
691
timeout : opts.timeout
692
cb : f
693
694
695
#################################################
696
# Account Management
697
#################################################
698
create_account: (opts) =>
699
opts = defaults opts,
700
first_name : required
701
last_name : required
702
email_address : required
703
password : required
704
agreed_to_terms: required
705
token : undefined # only required if an admin set the account creation token.
706
timeout : 40
707
cb : required
708
709
if not opts.agreed_to_terms
710
opts.cb(undefined, message.account_creation_failed(reason:{"agreed_to_terms":"Agree to the CoCalc Terms of Service."}))
711
return
712
713
if @_create_account_lock
714
# don't allow more than one create_account message at once -- see https://github.com/sagemathinc/cocalc/issues/1187
715
opts.cb(undefined, message.account_creation_failed(reason:{"account_creation_failed":"You are submitting too many requests to create an account; please wait a second."}))
716
return
717
718
@_create_account_lock = true
719
@call
720
message : message.create_account
721
first_name : opts.first_name
722
last_name : opts.last_name
723
email_address : opts.email_address
724
password : opts.password
725
agreed_to_terms : opts.agreed_to_terms
726
token : opts.token
727
timeout : opts.timeout
728
cb : (err, resp) =>
729
setTimeout((() => delete @_create_account_lock), 1500)
730
opts.cb(err, resp)
731
732
delete_account: (opts) =>
733
opts = defaults opts,
734
account_id : required
735
timeout : 40
736
cb : required
737
738
@call
739
message : message.delete_account
740
account_id : opts.account_id
741
timeout : opts.timeout
742
cb : opts.cb
743
744
sign_in_using_auth_token: (opts) ->
745
opts = defaults opts,
746
auth_token : required
747
cb : required
748
@call
749
message : message.sign_in_using_auth_token
750
auth_token : opts.auth_token
751
timeout : opts.timeout
752
cb : opts.cb
753
754
sign_in: (opts) ->
755
opts = defaults opts,
756
email_address : required
757
password : required
758
remember_me : false
759
cb : required
760
timeout : 40
761
762
@call
763
message : message.sign_in
764
email_address : opts.email_address
765
password : opts.password
766
remember_me : opts.remember_me
767
timeout : opts.timeout
768
cb : opts.cb
769
770
sign_out: (opts) ->
771
opts = defaults opts,
772
everywhere : false
773
cb : undefined
774
timeout : DEFAULT_TIMEOUT # seconds
775
776
@account_id = undefined
777
778
@call
779
message : message.sign_out(everywhere:opts.everywhere)
780
timeout : opts.timeout
781
cb : opts.cb
782
783
@emit('signed_out')
784
785
change_password: (opts) ->
786
opts = defaults opts,
787
email_address : required
788
old_password : ""
789
new_password : required
790
cb : undefined
791
@call
792
message : message.change_password
793
email_address : opts.email_address
794
old_password : opts.old_password
795
new_password : opts.new_password
796
cb : opts.cb
797
798
change_email: (opts) ->
799
opts = defaults opts,
800
new_email_address : required
801
password : ""
802
cb : undefined
803
if not @account_id?
804
opts.cb?("must be logged in")
805
return
806
@call
807
message: message.change_email_address
808
account_id : @account_id
809
new_email_address : opts.new_email_address
810
password : opts.password
811
error_event : true
812
cb : opts.cb
813
814
# forgot password -- send forgot password request to server
815
forgot_password: (opts) ->
816
opts = defaults opts,
817
email_address : required
818
cb : required
819
@call
820
message: message.forgot_password
821
email_address : opts.email_address
822
cb: opts.cb
823
824
# forgot password -- send forgot password request to server
825
reset_forgot_password: (opts) ->
826
opts = defaults(opts,
827
reset_code : required
828
new_password : required
829
cb : required
830
timeout : DEFAULT_TIMEOUT # seconds
831
)
832
@call(
833
message : message.reset_forgot_password(reset_code:opts.reset_code, new_password:opts.new_password)
834
cb : opts.cb
835
)
836
837
# forget about a given passport authentication strategy for this user
838
unlink_passport: (opts) ->
839
opts = defaults opts,
840
strategy : required
841
id : required
842
cb : undefined
843
@call
844
message : message.unlink_passport
845
strategy : opts.strategy
846
id : opts.id
847
error_event : true
848
timeout : 15
849
cb : opts.cb
850
851
api_key: (opts) ->
852
# getting, setting, deleting, etc., the api key for this account
853
opts = defaults opts,
854
action : required # 'get', 'delete', 'regenerate'
855
password : required
856
cb : required
857
if not @account_id?
858
opts.cb?("must be logged in")
859
return
860
@call
861
message: message.api_key
862
action : opts.action
863
password : opts.password
864
error_event : true
865
timeout : 10
866
cb : (err, resp) ->
867
opts.cb(err, resp?.api_key)
868
869
###
870
Project Management
871
###
872
create_project: (opts) =>
873
opts = defaults opts,
874
title : required
875
description : required
876
cb : undefined
877
@call
878
message: message.create_project(title:opts.title, description:opts.description)
879
cb : (err, resp) =>
880
if err
881
opts.cb?(err)
882
else if resp.event == 'error'
883
opts.cb?(resp.error)
884
else
885
opts.cb?(undefined, resp.project_id)
886
887
#################################################
888
# Individual Projects
889
#################################################
890
891
open_project: (opts) ->
892
opts = defaults opts,
893
project_id : required
894
cb : required
895
@call
896
message :
897
message.open_project
898
project_id : opts.project_id
899
cb : opts.cb
900
901
write_text_file_to_project: (opts) ->
902
opts = defaults opts,
903
project_id : required
904
path : required
905
content : required
906
timeout : DEFAULT_TIMEOUT
907
cb : undefined
908
909
@call
910
message :
911
message.write_text_file_to_project
912
project_id : opts.project_id
913
path : opts.path
914
content : opts.content
915
timeout : opts.timeout
916
cb : (err, resp) => opts.cb?(err, resp)
917
918
read_text_file_from_project: (opts) ->
919
opts = defaults opts,
920
project_id : required
921
path : required
922
cb : required
923
timeout : DEFAULT_TIMEOUT
924
925
@call
926
message :
927
message.read_text_file_from_project
928
project_id : opts.project_id
929
path : opts.path
930
timeout : opts.timeout
931
cb : opts.cb
932
933
# Like "read_text_file_from_project" above, except the callback
934
# message gives a url from which the file can be
935
# downloaded using standard AJAX.
936
# Despite the callback, this function is NOT asynchronous (that was for historical reasons).
937
# It also just returns the url.
938
read_file_from_project: (opts) ->
939
opts = defaults opts,
940
project_id : required
941
path : required
942
timeout : DEFAULT_TIMEOUT
943
archive : 'tar.bz2' # NOT SUPPORTED ANYMORE! -- when path is a directory: 'tar', 'tar.bz2', 'tar.gz', 'zip', '7z'
944
cb : undefined
945
946
base = window?.app_base_url ? '' # will be defined in web browser
947
if opts.path[0] == '/'
948
# absolute path to the root
949
opts.path = '.smc/root' + opts.path # use root symlink, which is created by start_smc
950
url = misc.encode_path("#{base}/#{opts.project_id}/raw/#{opts.path}")
951
opts.cb?(false, {url:url})
952
return url
953
954
project_branch_op: (opts) ->
955
opts = defaults opts,
956
project_id : required
957
branch : required
958
op : required
959
cb : required
960
@call
961
message : message["#{opts.op}_project_branch"]
962
project_id : opts.project_id
963
branch : opts.branch
964
cb : opts.cb
965
966
967
stopped_editing_file: (opts) =>
968
opts = defaults opts,
969
project_id : required
970
filename : required
971
cb : undefined
972
@call
973
message : message.stopped_editing_file
974
project_id : opts.project_id
975
filename : opts.filename
976
cb : opts.cb
977
978
invite_noncloud_collaborators: (opts) =>
979
opts = defaults opts,
980
project_id : required
981
title : required
982
link2proj : required
983
replyto : undefined
984
replyto_name : undefined
985
to : required
986
email : required # body in HTML format
987
subject : undefined
988
cb : required
989
990
@call
991
message: message.invite_noncloud_collaborators
992
project_id : opts.project_id
993
title : opts.title
994
link2proj : opts.link2proj
995
email : opts.email
996
replyto : opts.replyto
997
replyto_name : opts.replyto_name
998
to : opts.to
999
subject : opts.subject
1000
cb : (err, resp) =>
1001
if err
1002
opts.cb(err)
1003
else if resp.event == 'error'
1004
if not resp.error
1005
resp.error = "error inviting collaborators"
1006
opts.cb(resp.error)
1007
else
1008
opts.cb(undefined, resp)
1009
1010
copy_path_between_projects: (opts) =>
1011
opts = defaults opts,
1012
public : false
1013
src_project_id : required # id of source project
1014
src_path : required # relative path of director or file in the source project
1015
target_project_id : required # if of target project
1016
target_path : undefined # defaults to src_path
1017
overwrite_newer : false # overwrite newer versions of file at destination (destructive)
1018
delete_missing : false # delete files in dest that are missing from source (destructive)
1019
backup : false # make ~ backup files instead of overwriting changed files
1020
timeout : undefined # how long to wait for the copy to complete before reporting "error" (though it could still succeed)
1021
exclude_history : false # if true, exclude all files of the form *.sage-history
1022
cb : undefined # cb(err)
1023
1024
is_public = opts.public
1025
delete opts.public
1026
cb = opts.cb
1027
delete opts.cb
1028
1029
if not opts.target_path?
1030
opts.target_path = opts.src_path
1031
1032
if is_public
1033
mesg = message.copy_public_path_between_projects(opts)
1034
else
1035
mesg = message.copy_path_between_projects(opts)
1036
1037
@call
1038
message : mesg
1039
cb : (err, resp) =>
1040
if err
1041
cb?(err)
1042
else if resp.event == 'error'
1043
cb?(resp.error)
1044
else
1045
cb?(undefined, resp)
1046
1047
# Set a quota parameter for a given project.
1048
# As of now, only user in the admin group can make these changes.
1049
project_set_quotas: (opts) =>
1050
opts = defaults opts,
1051
project_id : required
1052
memory : undefined # see message.coffee for the units, etc., for all these settings
1053
cpu_shares : undefined
1054
cores : undefined
1055
disk_quota : undefined
1056
mintime : undefined
1057
network : undefined
1058
member_host : undefined
1059
cb : undefined
1060
cb = opts.cb
1061
delete opts.cb
1062
1063
@call
1064
message : message.project_set_quotas(opts)
1065
cb : (err, resp) =>
1066
if err
1067
cb?(err)
1068
else if resp.event == 'error'
1069
cb?(resp.error)
1070
else
1071
cb?(undefined, resp)
1072
1073
#################################################
1074
# Blobs
1075
#################################################
1076
remove_blob_ttls: (opts) =>
1077
opts = defaults opts,
1078
uuids : required # list of sha1 hashes of blobs stored in the blobstore
1079
cb : undefined
1080
if opts.uuids.length == 0
1081
opts.cb?()
1082
else
1083
@call
1084
message :
1085
message.remove_blob_ttls
1086
uuids : opts.uuids
1087
cb : (err, resp) =>
1088
if err
1089
opts.cb?(err)
1090
else if resp.event == 'error'
1091
opts.cb?(resp.error)
1092
else
1093
opts.cb?()
1094
1095
1096
#################################################
1097
# *PUBLIC* Projects
1098
#################################################
1099
1100
public_get_text_file: (opts) =>
1101
opts = defaults opts,
1102
project_id : required
1103
path : required
1104
cb : required
1105
timeout : DEFAULT_TIMEOUT
1106
1107
@call
1108
message :
1109
message.public_get_text_file
1110
project_id : opts.project_id
1111
path : opts.path
1112
timeout : opts.timeout
1113
cb : (err, resp) =>
1114
if err
1115
opts.cb(err)
1116
else if resp.event == 'error'
1117
opts.cb(resp.error)
1118
else
1119
opts.cb(undefined, resp.data)
1120
1121
public_project_directory_listing: (opts) =>
1122
opts = defaults opts,
1123
project_id : required
1124
path : '.'
1125
time : false
1126
start : 0
1127
limit : -1
1128
timeout : DEFAULT_TIMEOUT
1129
hidden : false
1130
cb : required
1131
@call
1132
message :
1133
message.public_get_directory_listing
1134
project_id : opts.project_id
1135
path : opts.path
1136
time : opts.time
1137
start : opts.tart
1138
limit : opts.limit
1139
hidden : opts.hidden
1140
timeout : opts.timeout
1141
cb : (err, resp) =>
1142
if err
1143
opts.cb(err)
1144
else if resp.event == 'error'
1145
opts.cb(resp.error)
1146
else
1147
opts.cb(undefined, resp.result)
1148
1149
######################################################################
1150
# Execute a program in a given project
1151
######################################################################
1152
exec: (opts) ->
1153
opts = defaults opts,
1154
project_id : required
1155
path : ''
1156
command : required
1157
args : []
1158
timeout : 30
1159
network_timeout : undefined
1160
max_output : undefined
1161
bash : false
1162
err_on_exit : true
1163
cb : required # cb(err, {stdout:..., stderr:..., exit_code:...}).
1164
1165
if not opts.network_timeout?
1166
opts.network_timeout = opts.timeout * 1.5
1167
1168
#console.log("Executing -- #{opts.command}, #{misc.to_json(opts.args)} in '#{opts.path}'")
1169
@call
1170
message : message.project_exec
1171
project_id : opts.project_id
1172
path : opts.path
1173
command : opts.command
1174
args : opts.args
1175
timeout : opts.timeout
1176
max_output : opts.max_output
1177
bash : opts.bash
1178
err_on_exit : opts.err_on_exit
1179
timeout : opts.network_timeout
1180
cb : (err, mesg) ->
1181
#console.log("Executing #{opts.command}, #{misc.to_json(opts.args)} -- got back: #{err}, #{misc.to_json(mesg)}")
1182
if err
1183
opts.cb(err, mesg)
1184
else if mesg.event == 'error'
1185
opts.cb(mesg.error)
1186
else
1187
opts.cb(false, {stdout:mesg.stdout, stderr:mesg.stderr, exit_code:mesg.exit_code})
1188
1189
makedirs: (opts) =>
1190
opts = defaults opts,
1191
project_id : required
1192
path : required
1193
cb : undefined # (err)
1194
@exec
1195
project_id : opts.project_id
1196
command : 'mkdir'
1197
args : ['-p', opts.path]
1198
cb : opts.cb
1199
1200
# find directories and subdirectories matching a given query
1201
find_directories: (opts) =>
1202
opts = defaults opts,
1203
project_id : required
1204
query : '*' # see the -iname option to the UNIX find command.
1205
path : '.' # Root path to find directories from
1206
exclusions : undefined # Array<String> Paths relative to `opts.path`. Skips whole sub-trees
1207
include_hidden : false
1208
cb : required # cb(err, object describing result (see code below))
1209
1210
args = [opts.path, '-xdev', '!', '-readable', '-prune', '-o', '-type', 'd', '-iname', "'#{opts.query}'", '-readable']
1211
tail_args = ['-print']
1212
1213
if opts.exclusions?
1214
exclusion_args = _.map opts.exclusions, (excluded_path, index) =>
1215
"-a -not \\( -path '#{opts.path}/#{excluded_path}' -prune \\)"
1216
args = args.concat(exclusion_args)
1217
1218
args = args.concat(tail_args)
1219
command = "find #{args.join(' ')}"
1220
1221
@exec
1222
project_id : opts.project_id
1223
command : command
1224
timeout : 15
1225
cb : (err, result) =>
1226
if err
1227
opts.cb?(err); return
1228
if result.event == 'error'
1229
opts.cb?(result.error); return
1230
n = opts.path.length + 1
1231
v = result.stdout.split('\n')
1232
if not opts.include_hidden
1233
v = (x for x in v when x.indexOf('/.') == -1)
1234
v = (x.slice(n) for x in v when x.length > n)
1235
ans =
1236
query : opts.query
1237
path : opts.path
1238
project_id : opts.project_id
1239
directories : v
1240
opts.cb?(undefined, ans)
1241
1242
#################################################
1243
# Search / user info
1244
#################################################
1245
1246
user_search: (opts) =>
1247
opts = defaults opts,
1248
query : required
1249
query_id : -1 # So we can check that it matches the most recent query
1250
limit : 20
1251
timeout : DEFAULT_TIMEOUT
1252
cb : required
1253
1254
@call
1255
message : message.user_search(query:opts.query, limit:opts.limit)
1256
timeout : opts.timeout
1257
cb : (err, resp) =>
1258
if err
1259
opts.cb(err)
1260
else
1261
opts.cb(undefined, resp.results, opts.query_id)
1262
1263
project_invite_collaborator: (opts) =>
1264
opts = defaults opts,
1265
project_id : required
1266
account_id : required
1267
cb : (err) =>
1268
@call
1269
message : message.invite_collaborator(project_id:opts.project_id, account_id:opts.account_id)
1270
cb : (err, result) =>
1271
if err
1272
opts.cb(err)
1273
else if result.event == 'error'
1274
opts.cb(result.error)
1275
else
1276
opts.cb(undefined, result)
1277
1278
project_remove_collaborator: (opts) =>
1279
opts = defaults opts,
1280
project_id : required
1281
account_id : required
1282
cb : (err) =>
1283
1284
@call
1285
message : message.remove_collaborator(project_id:opts.project_id, account_id:opts.account_id)
1286
cb : (err, result) =>
1287
if err
1288
opts.cb(err)
1289
else if result.event == 'error'
1290
opts.cb(result.error)
1291
else
1292
opts.cb(undefined, result)
1293
1294
############################################
1295
# Bulk information about several projects or accounts
1296
# (may be used by chat, etc.)
1297
# NOTE:
1298
# When get_projects is called (which happens regularly), any info about
1299
# project titles or "account_id --> name" mappings gets updated. So
1300
# usually get_project_titles and get_usernames doesn't even have
1301
# to make a call to the server. A case where it would is when rendering
1302
# the notifications and the project list hasn't been returned. Also,
1303
# at some point, project list will probably just return the most recent
1304
# projects or partial info about them.
1305
#############################################
1306
1307
get_usernames: (opts) ->
1308
opts = defaults opts,
1309
account_ids : required
1310
use_cache : true
1311
cb : required # cb(err, map from account_id to {first_name:?, last_name:?})
1312
usernames = {}
1313
for account_id in opts.account_ids
1314
usernames[account_id] = false
1315
if opts.use_cache
1316
for account_id, done of usernames
1317
if not done and @_usernames_cache[account_id]?
1318
usernames[account_id] = @_usernames_cache[account_id]
1319
account_ids = (account_id for account_id, done of usernames when not done)
1320
if account_ids.length == 0
1321
opts.cb(undefined, usernames)
1322
else
1323
@call
1324
message : message.get_usernames(account_ids : account_ids)
1325
cb : (err, resp) =>
1326
if err
1327
opts.cb(err)
1328
else if resp.event == 'error'
1329
opts.cb(resp.error)
1330
else
1331
for account_id, username of resp.usernames
1332
usernames[account_id] = username
1333
@_usernames_cache[account_id] = username # TODO: we could expire this cache...
1334
opts.cb(undefined, usernames)
1335
1336
#################################################
1337
# File Management
1338
#################################################
1339
project_directory_listing: (opts) =>
1340
opts = defaults opts,
1341
project_id : required
1342
path : '.'
1343
timeout : 60 # ignored
1344
hidden : false
1345
cb : required
1346
base = window?.app_base_url ? '' # will be defined in web browser
1347
if opts.path[0] == '/'
1348
opts.path = '.smc/root' + opts.path # use root symlink, which is created by start_smc
1349
url = misc.encode_path("#{base}/#{opts.project_id}/raw/.smc/directory_listing/#{opts.path}")
1350
url += "?random=#{Math.random()}"
1351
if opts.hidden
1352
url += '&hidden=true'
1353
#console.log(url)
1354
req = $.ajax
1355
dataType : "json"
1356
url : url
1357
timeout : 3000
1358
success : (data) ->
1359
#console.log('success')
1360
opts.cb(undefined, data)
1361
req.fail (err) ->
1362
#console.log('fail')
1363
opts.cb(err)
1364
1365
project_get_state: (opts) =>
1366
opts = defaults opts,
1367
project_id : required
1368
cb : required # cb(err, utc_seconds_epoch)
1369
@call
1370
message:
1371
message.project_get_state
1372
project_id : opts.project_id
1373
cb : (err, resp) ->
1374
if err
1375
opts.cb(err)
1376
else if resp.event == 'error'
1377
opts.cb(resp.error)
1378
else
1379
opts.cb(false, resp.state)
1380
1381
#################################################
1382
# Print file to pdf
1383
# The printed version of the file will be created in the same directory
1384
# as path, but with extension replaced by ".pdf".
1385
#################################################
1386
print_to_pdf: (opts) =>
1387
opts = defaults opts,
1388
project_id : required
1389
path : required
1390
timeout : 90 # client timeout -- some things can take a long time to print!
1391
options : undefined # optional options that get passed to the specific backend for this file type
1392
cb : undefined # cp(err, relative path in project to printed file)
1393
opts.options.timeout = opts.timeout # timeout on backend
1394
@call_local_hub
1395
project_id : opts.project_id
1396
message : message.print_to_pdf
1397
path : opts.path
1398
options : opts.options
1399
timeout : opts.timeout
1400
cb : (err, resp) =>
1401
if err
1402
opts.cb?(err)
1403
else if resp.event == 'error'
1404
if resp.error?
1405
opts.cb?(resp.error)
1406
else
1407
opts.cb?('error')
1408
else
1409
opts.cb?(undefined, resp.path)
1410
1411
1412
#################################################
1413
# Bad situation error loging
1414
#################################################
1415
log_error: (error) =>
1416
@call(message : message.log_client_error(error:error))
1417
1418
webapp_error: (opts) =>
1419
@call(message : message.webapp_error(opts))
1420
1421
1422
######################################################################
1423
# stripe payments api
1424
######################################################################
1425
# gets custormer info (if any) and stripe public api key
1426
# for this user, if they are logged in
1427
_stripe_call: (mesg, cb) =>
1428
@call
1429
message : mesg
1430
error_event : true
1431
timeout : 15
1432
cb : cb
1433
1434
stripe_get_customer: (opts) =>
1435
opts = defaults opts,
1436
cb : required
1437
@_stripe_call message.stripe_get_customer(), (err, mesg) =>
1438
if err
1439
opts.cb(err)
1440
else
1441
resp =
1442
stripe_publishable_key : mesg.stripe_publishable_key
1443
customer : mesg.customer
1444
opts.cb(undefined, resp)
1445
1446
stripe_create_source: (opts) =>
1447
opts = defaults opts,
1448
token : required
1449
cb : required
1450
@_stripe_call(message.stripe_create_source(token: opts.token), opts.cb)
1451
1452
stripe_delete_source: (opts) =>
1453
opts = defaults opts,
1454
card_id : required
1455
cb : required
1456
@_stripe_call(message.stripe_delete_source(card_id: opts.card_id), opts.cb)
1457
1458
stripe_update_source: (opts) =>
1459
opts = defaults opts,
1460
card_id : required
1461
info : required
1462
cb : required
1463
@_stripe_call(message.stripe_update_source(card_id: opts.card_id, info:opts.info), opts.cb)
1464
1465
stripe_set_default_source: (opts) =>
1466
opts = defaults opts,
1467
card_id : required
1468
cb : required
1469
@_stripe_call(message.stripe_set_default_source(card_id: opts.card_id), opts.cb)
1470
1471
# gets list of past stripe charges for this account.
1472
stripe_get_charges: (opts) =>
1473
opts = defaults opts,
1474
limit : undefined # between 1 and 100 (default: 10)
1475
ending_before : undefined # see https://stripe.com/docs/api/node#list_charges
1476
starting_after : undefined
1477
cb : required
1478
@call
1479
message :
1480
message.stripe_get_charges
1481
limit : opts.limit
1482
ending_before : opts.ending_before
1483
starting_after : opts.starting_after
1484
error_event : true
1485
cb : (err, mesg) =>
1486
if err
1487
opts.cb(err)
1488
else
1489
opts.cb(undefined, mesg.charges)
1490
1491
# gets stripe plans that could be subscribed to.
1492
stripe_get_plans: (opts) =>
1493
opts = defaults opts,
1494
cb : required
1495
@call
1496
message : message.stripe_get_plans()
1497
error_event : true
1498
cb : (err, mesg) =>
1499
if err
1500
opts.cb(err)
1501
else
1502
opts.cb(undefined, mesg.plans)
1503
1504
stripe_create_subscription: (opts) =>
1505
opts = defaults opts,
1506
plan : required
1507
quantity : 1
1508
coupon : undefined
1509
cb : required
1510
@call
1511
message : message.stripe_create_subscription
1512
plan : opts.plan
1513
quantity : opts.quantity
1514
coupon : opts.coupon
1515
error_event : true
1516
cb : opts.cb
1517
1518
stripe_cancel_subscription: (opts) =>
1519
opts = defaults opts,
1520
subscription_id : required
1521
at_period_end : true
1522
cb : required
1523
@call
1524
message : message.stripe_cancel_subscription
1525
subscription_id : opts.subscription_id
1526
at_period_end : opts.at_period_end
1527
error_event : true
1528
cb : opts.cb
1529
1530
stripe_update_subscription: (opts) =>
1531
opts = defaults opts,
1532
subscription_id : required
1533
quantity : undefined # if given, must be >= number of projects
1534
coupon : undefined
1535
projects : undefined # ids of projects that subscription applies to
1536
plan : undefined
1537
cb : required
1538
@call
1539
message : message.stripe_update_subscription
1540
subscription_id : opts.subscription_id
1541
quantity : opts.quantity
1542
coupon : opts.coupon
1543
projects : opts.projects
1544
plan : opts.plan
1545
error_event : true
1546
cb : opts.cb
1547
1548
# gets list of past stripe charges for this account.
1549
stripe_get_subscriptions: (opts) =>
1550
opts = defaults opts,
1551
limit : undefined # between 1 and 100 (default: 10)
1552
ending_before : undefined # see https://stripe.com/docs/api/node#list_subscriptions
1553
starting_after : undefined
1554
cb : required
1555
@call
1556
message :
1557
message.stripe_get_subscriptions
1558
limit : opts.limit
1559
ending_before : opts.ending_before
1560
starting_after : opts.starting_after
1561
error_event : true
1562
cb : (err, mesg) =>
1563
if err
1564
opts.cb(err)
1565
else
1566
opts.cb(undefined, mesg.subscriptions)
1567
1568
# gets list of invoices for this account.
1569
stripe_get_invoices: (opts) =>
1570
opts = defaults opts,
1571
limit : 10 # between 1 and 100 (default: 10)
1572
ending_before : undefined # see https://stripe.com/docs/api/node#list_charges
1573
starting_after : undefined
1574
cb : required
1575
@call
1576
message :
1577
message.stripe_get_invoices
1578
limit : opts.limit
1579
ending_before : opts.ending_before
1580
starting_after : opts.starting_after
1581
error_event : true
1582
cb : (err, mesg) =>
1583
if err
1584
opts.cb(err)
1585
else
1586
opts.cb(undefined, mesg.invoices)
1587
1588
stripe_admin_create_invoice_item: (opts) =>
1589
opts = defaults opts,
1590
account_id : undefined # one of account_id or email_address must be given
1591
email_address : undefined
1592
amount : undefined # in US dollars -- if amount/description not given, then merely ensures user has stripe account
1593
description : undefined
1594
cb : required
1595
@call
1596
message : message.stripe_admin_create_invoice_item
1597
account_id : opts.account_id
1598
email_address : opts.email_address
1599
amount : opts.amount
1600
description : opts.description
1601
error_event : true
1602
cb : opts.cb
1603
1604
# Make it so the SMC user with the given email address has a corresponding stripe
1605
# identity, even if they have never entered a credit card. May only be used by
1606
# admin users.
1607
stripe_admin_create_customer: (opts) =>
1608
opts = defaults opts,
1609
account_id : undefined # one of account_id or email_address must be given
1610
email_address : undefined
1611
cb : required
1612
@stripe_admin_create_invoice_item(opts)
1613
1614
# Support Tickets
1615
1616
create_support_ticket: ({opts, cb}) =>
1617
@call
1618
message : message.create_support_ticket(opts)
1619
timeout : 20
1620
error_event : true
1621
cb : (err, resp) ->
1622
if err
1623
cb?(err)
1624
else
1625
cb?(undefined, resp.url)
1626
1627
get_support_tickets: (cb) =>
1628
@call
1629
message : message.get_support_tickets()
1630
timeout : 20
1631
error_event : true
1632
cb : (err, tickets) ->
1633
if err
1634
cb?(err)
1635
else
1636
cb?(undefined, tickets.tickets)
1637
1638
# Queries directly to the database (sort of like Facebook's GraphQL)
1639
1640
projects: (opts) =>
1641
opts = defaults opts,
1642
cb : required
1643
@query
1644
query :
1645
projects : [{project_id:null, title:null, description:null, last_edited:null, users:null}]
1646
changes : true
1647
cb : opts.cb
1648
1649
changefeed: (opts) =>
1650
keys = misc.keys(opts)
1651
if keys.length != 1
1652
throw Error("must specify exactly one table")
1653
table = keys[0]
1654
x = {}
1655
if not misc.is_array(opts[table])
1656
x[table] = [opts[table]]
1657
else
1658
x[table] = opts[table]
1659
return @query(query:x, changes: true)
1660
1661
sync_table: (query, options, debounce_interval=2000, throttle_changes=undefined) =>
1662
return synctable.sync_table(query, options, @, debounce_interval, throttle_changes)
1663
1664
sync_string: (opts) =>
1665
opts = defaults opts,
1666
id : undefined
1667
project_id : required
1668
path : required
1669
file_use_interval : 'default'
1670
cursors : false
1671
patch_interval : 1000
1672
opts.client = @
1673
return new syncstring.SyncString(opts)
1674
1675
sync_db: (opts) =>
1676
opts = defaults opts,
1677
project_id : required
1678
path : required
1679
primary_keys : required
1680
string_cols : undefined
1681
cursors : false
1682
change_throttle : 500 # amount to throttle change events (in ms)
1683
save_interval : 2000 # amount to debounce saves (in ms)
1684
patch_interval : 1000
1685
opts.client = @
1686
return new db_doc.SyncDB(opts)
1687
1688
open_existing_sync_document: (opts) =>
1689
opts = defaults opts,
1690
project_id : required
1691
path : required
1692
cb : required # cb(err, document)
1693
opts.client = @
1694
db_doc.open_existing_sync_document(opts)
1695
return
1696
1697
# If called on the fronted, will make the given file with the given action.
1698
# Does nothing on the backend.
1699
mark_file: (opts) =>
1700
opts = defaults opts,
1701
project_id : required
1702
path : required
1703
action : required
1704
ttl : 120
1705
# Will only do something if @_redux has been set.
1706
@_redux?.getActions('file_use').mark_file(opts.project_id, opts.path, opts.action, opts.ttl)
1707
1708
query: (opts) =>
1709
opts = defaults opts,
1710
query : required
1711
changes : undefined
1712
options : undefined # if given must be an array of objects, e.g., [{limit:5}]
1713
timeout : 30
1714
cb : undefined
1715
if opts.options? and not misc.is_array(opts.options)
1716
throw Error("options must be an array")
1717
#console.log("query=#{misc.to_json(opts.query)}")
1718
err = validate_client_query(opts.query, @account_id)
1719
if err
1720
opts.cb?(err)
1721
return
1722
mesg = message.query
1723
query : opts.query
1724
options : opts.options
1725
changes : opts.changes
1726
multi_response : opts.changes
1727
@call
1728
message : mesg
1729
error_event : true
1730
timeout : opts.timeout
1731
cb : opts.cb
1732
1733
query_cancel: (opts) =>
1734
opts = defaults opts,
1735
id : required
1736
cb : undefined
1737
@call
1738
message : message.query_cancel(id:opts.id)
1739
error_event : true
1740
timeout : 30
1741
cb : opts.cb
1742
1743
query_get_changefeed_ids: (opts) =>
1744
opts = defaults opts,
1745
cb : required
1746
@call
1747
message : message.query_get_changefeed_ids()
1748
error_event : true
1749
timeout : 30
1750
cb : (err, resp) =>
1751
if err
1752
opts.cb(err)
1753
else
1754
@_changefeed_ids = resp.changefeed_ids
1755
opts.cb(undefined, resp.changefeed_ids)
1756
1757
#################################################
1758
# Other account Management functionality shared between client and server
1759
#################################################
1760
exports.is_valid_password = (password) ->
1761
if typeof(password) != 'string'
1762
return [false, 'Password must be specified.']
1763
if password.length >= 6 and password.length <= 64
1764
return [true, '']
1765
else
1766
return [false, 'Password must be between 6 and 64 characters in length.']
1767
1768
exports.issues_with_create_account = (mesg) ->
1769
issues = {}
1770
if not mesg.agreed_to_terms
1771
issues.agreed_to_terms = 'Agree to the Salvus Terms of Service.'
1772
if mesg.first_name == ''
1773
issues.first_name = 'Enter your first name.'
1774
if mesg.last_name == ''
1775
issues.last_name = 'Enter your last name.'
1776
if not misc.is_valid_email_address(mesg.email_address)
1777
issues.email_address = 'Email address does not appear to be valid.'
1778
[valid, reason] = exports.is_valid_password(mesg.password)
1779
if not valid
1780
issues.password = reason
1781
return issues
1782
1783
1784
1785
1786