agi.sharedspace.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. package agi
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "sort"
  9. "github.com/robertkrimen/otto"
  10. "imuslab.com/arozos/mod/agi/static"
  11. "imuslab.com/arozos/mod/info/logger"
  12. "imuslab.com/arozos/mod/sharedspace"
  13. )
  14. /*
  15. AGI SharedSpace Library
  16. Exposes the shared collaboration space manager to AGI scripts. A shared
  17. space is an area where multiple different users can share texts, images,
  18. files and collaboratively edited documents together. Spaces carry an
  19. access mode - "open" (the random space ID acts as the capability),
  20. "public" (discoverable, self-join) or "private" (members only) - plus
  21. a member list with owner / admin / member roles, a metadata store and
  22. an optional persistent flag (survives restarts). Loaded with
  23. requirelib("sharedspace").
  24. MeetRoom binds one space to every meeting room (see agi.meetroom.go),
  25. so this library is also how scripts read a live meeting's chat and post
  26. messages / files into it.
  27. */
  28. // agiOriginTag marks items posted through this library so consumers (e.g.
  29. // the MeetRoom space item bridge) can tell them apart from their own posts.
  30. const agiOriginTag = "agi"
  31. func (g *Gateway) SharedSpaceLibRegister() {
  32. err := g.RegisterLib("sharedspace", g.injectSharedSpaceFunctions)
  33. if err != nil {
  34. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  35. os.Exit(1)
  36. }
  37. }
  38. // agiDescribeSpace renders the space fields shared with AGI scripts.
  39. func agiDescribeSpace(space *sharedspace.Space) map[string]interface{} {
  40. return map[string]interface{}{
  41. "spaceid": space.ID,
  42. "name": space.Name,
  43. "owner": space.Owner,
  44. "access": space.AccessMode(),
  45. "persistent": space.Persistent,
  46. "items": space.ItemCount(),
  47. "docs": space.DocCount(),
  48. "members": space.MemberCount(),
  49. "metadata": space.Metadata(),
  50. "createdat": space.CreatedAt.Unix(),
  51. }
  52. }
  53. // agiDescribeItem renders a space item for AGI scripts. Text content is
  54. // included inline for text items; blobs report their display name and size.
  55. func agiDescribeItem(item *sharedspace.Item) map[string]interface{} {
  56. return map[string]interface{}{
  57. "itemid": item.ID,
  58. "type": item.Type,
  59. "name": item.Name,
  60. "text": item.Text,
  61. "size": item.Size,
  62. "uploader": item.Uploader,
  63. "origin": item.Origin,
  64. "time": item.CreatedAt.Unix(),
  65. }
  66. }
  67. // agiDescribeDoc renders a document snapshot for AGI scripts.
  68. func agiDescribeDoc(doc *sharedspace.DocSnapshot, includeContent bool) map[string]interface{} {
  69. desc := map[string]interface{}{
  70. "docid": doc.ID,
  71. "name": doc.Name,
  72. "creator": doc.Creator,
  73. "revision": doc.Revision,
  74. "createdat": doc.CreatedAt.Unix(),
  75. "updatedat": doc.UpdatedAt.Unix(),
  76. "updatedby": doc.UpdatedBy,
  77. }
  78. if includeContent {
  79. desc["content"] = doc.Content
  80. }
  81. return desc
  82. }
  83. // resolveNotifyRecipients computes who should receive a space notification.
  84. // It starts from the space members, optionally narrows to an explicit target
  85. // set (nil = every member), and always drops the sender and anyone currently
  86. // connected to the space (they are already receiving the live message). The
  87. // result is sorted for deterministic delivery and testing.
  88. func resolveNotifyRecipients(members map[string]string, present map[string]bool, sender string, requested []string) []string {
  89. var requestedSet map[string]bool
  90. if requested != nil {
  91. requestedSet = make(map[string]bool, len(requested))
  92. for _, name := range requested {
  93. if name != "" {
  94. requestedSet[name] = true
  95. }
  96. }
  97. }
  98. recipients := []string{}
  99. for username := range members {
  100. if username == "" || username == sender {
  101. continue
  102. }
  103. if present != nil && present[username] {
  104. continue
  105. }
  106. if requestedSet != nil && !requestedSet[username] {
  107. continue
  108. }
  109. recipients = append(recipients, username)
  110. }
  111. sort.Strings(recipients)
  112. return recipients
  113. }
  114. func (g *Gateway) injectSharedSpaceFunctions(payload *static.AgiLibInjectionPayload) {
  115. vm := payload.VM
  116. u := payload.User
  117. scriptFsh := payload.ScriptFsh
  118. manager := g.Option.SharedSpaceManager
  119. if manager == nil || u == nil {
  120. return
  121. }
  122. //The sender label shown to the user is the script's module root (e.g.
  123. //"Chatspace"), matching the AGI notification library's convention.
  124. senderLabel := "Shared Space"
  125. if payload.ScriptPath != "" {
  126. senderLabel = static.GetScriptRoot(payload.ScriptPath, "./web/")
  127. }
  128. jsonReply := func(v interface{}) otto.Value {
  129. js, err := json.Marshal(v)
  130. if err != nil {
  131. return otto.NullValue()
  132. }
  133. val, _ := vm.ToValue(string(js))
  134. return val
  135. }
  136. //getSpace resolves the first argument to a live space without any
  137. //access check (used where the called method enforces its own ACL).
  138. getSpace := func(call otto.FunctionCall) (*sharedspace.Space, bool) {
  139. spaceID, err := call.Argument(0).ToString()
  140. if err != nil {
  141. return nil, false
  142. }
  143. return manager.GetSpace(spaceID)
  144. }
  145. //getReadableSpace additionally requires read access for the calling
  146. //user - private spaces stay invisible to outsiders.
  147. getReadableSpace := func(call otto.FunctionCall) (*sharedspace.Space, bool) {
  148. space, ok := getSpace(call)
  149. if !ok || !space.CanRead(u.Username) {
  150. return nil, false
  151. }
  152. return space, true
  153. }
  154. //optionalString reads an argument that may be omitted in the script.
  155. optionalString := func(call otto.FunctionCall, idx int) string {
  156. arg := call.Argument(idx)
  157. if !arg.IsDefined() {
  158. return ""
  159. }
  160. s, _ := arg.ToString()
  161. if s == "undefined" || s == "null" {
  162. return ""
  163. }
  164. return s
  165. }
  166. //Create a new open, ephemeral space owned by the calling user
  167. vm.Set("_sharedspace_createSpace", func(call otto.FunctionCall) otto.Value {
  168. name := optionalString(call, 0)
  169. space := manager.CreateSpace(u.Username, name)
  170. return jsonReply(agiDescribeSpace(space))
  171. })
  172. //Create a space with options: {access, persistent, metadata}
  173. vm.Set("_sharedspace_createSpaceAdvanced", func(call otto.FunctionCall) otto.Value {
  174. name := optionalString(call, 0)
  175. optionsJSON := optionalString(call, 1)
  176. options := struct {
  177. Access string `json:"access"`
  178. Persistent bool `json:"persistent"`
  179. Metadata map[string]string `json:"metadata"`
  180. }{}
  181. if optionsJSON != "" {
  182. json.Unmarshal([]byte(optionsJSON), &options)
  183. }
  184. space, err := manager.CreateSpaceWithOptions(u.Username, name, sharedspace.SpaceOptions{
  185. Access: options.Access,
  186. Persistent: options.Persistent,
  187. Metadata: options.Metadata,
  188. })
  189. if err != nil {
  190. g.RaiseError(err)
  191. return otto.NullValue()
  192. }
  193. return jsonReply(agiDescribeSpace(space))
  194. })
  195. //Delete a space; owner or space admin only
  196. vm.Set("_sharedspace_deleteSpace", func(call otto.FunctionCall) otto.Value {
  197. space, ok := getSpace(call)
  198. if !ok || !space.CanManage(u.Username) {
  199. return otto.FalseValue()
  200. }
  201. //Give live channel subscribers the shutdown notice first
  202. space.Channel().Broadcast([]byte(`{"type":"space-closed"}`), -1)
  203. manager.DeleteSpace(space.ID)
  204. return otto.TrueValue()
  205. })
  206. //List the spaces owned by the calling user
  207. vm.Set("_sharedspace_listMySpaces", func(call otto.FunctionCall) otto.Value {
  208. owned := manager.ListSpacesByOwner(u.Username)
  209. list := make([]map[string]interface{}, 0, len(owned))
  210. for _, space := range owned {
  211. list = append(list, agiDescribeSpace(space))
  212. }
  213. return jsonReply(list)
  214. })
  215. //List the spaces the calling user has joined (owner, admin or member)
  216. vm.Set("_sharedspace_listJoinedSpaces", func(call otto.FunctionCall) otto.Value {
  217. joined := manager.ListSpacesByMember(u.Username)
  218. list := make([]map[string]interface{}, 0, len(joined))
  219. for _, space := range joined {
  220. list = append(list, agiDescribeSpace(space))
  221. }
  222. return jsonReply(list)
  223. })
  224. //List every public space (the discovery directory)
  225. vm.Set("_sharedspace_listPublicSpaces", func(call otto.FunctionCall) otto.Value {
  226. public := manager.ListPublicSpaces()
  227. list := make([]map[string]interface{}, 0, len(public))
  228. for _, space := range public {
  229. list = append(list, agiDescribeSpace(space))
  230. }
  231. return jsonReply(list)
  232. })
  233. //Describe a space by ID
  234. vm.Set("_sharedspace_getSpaceInfo", func(call otto.FunctionCall) otto.Value {
  235. space, ok := getReadableSpace(call)
  236. if !ok {
  237. return jsonReply(map[string]interface{}{"exists": false})
  238. }
  239. desc := agiDescribeSpace(space)
  240. desc["exists"] = true
  241. role, isMember := space.Role(u.Username)
  242. desc["myrole"] = role
  243. desc["ismember"] = isMember
  244. return jsonReply(desc)
  245. })
  246. //Self-join a public (or open) space
  247. vm.Set("_sharedspace_joinSpace", func(call otto.FunctionCall) otto.Value {
  248. space, ok := getSpace(call)
  249. if !ok {
  250. return otto.FalseValue()
  251. }
  252. if err := space.JoinPublic(u.Username); err != nil {
  253. return otto.FalseValue()
  254. }
  255. return otto.TrueValue()
  256. })
  257. //Leave a space
  258. vm.Set("_sharedspace_leaveSpace", func(call otto.FunctionCall) otto.Value {
  259. space, ok := getSpace(call)
  260. if !ok {
  261. return otto.FalseValue()
  262. }
  263. if err := space.RemoveMember(u.Username, u.Username); err != nil {
  264. return otto.FalseValue()
  265. }
  266. return otto.TrueValue()
  267. })
  268. //Change the access mode; space managers only
  269. vm.Set("_sharedspace_setAccess", func(call otto.FunctionCall) otto.Value {
  270. space, ok := getSpace(call)
  271. if !ok {
  272. return otto.FalseValue()
  273. }
  274. access, err := call.Argument(1).ToString()
  275. if err != nil {
  276. return otto.FalseValue()
  277. }
  278. if err := space.SetAccess(u.Username, access); err != nil {
  279. return otto.FalseValue()
  280. }
  281. return otto.TrueValue()
  282. })
  283. //Set (or with an empty value delete) a metadata entry; managers only
  284. vm.Set("_sharedspace_setMeta", func(call otto.FunctionCall) otto.Value {
  285. space, ok := getSpace(call)
  286. if !ok {
  287. return otto.FalseValue()
  288. }
  289. key, err := call.Argument(1).ToString()
  290. if err != nil {
  291. return otto.FalseValue()
  292. }
  293. value := optionalString(call, 2)
  294. if err := space.SetMeta(u.Username, key, value); err != nil {
  295. return otto.FalseValue()
  296. }
  297. return otto.TrueValue()
  298. })
  299. //Read the metadata of a space
  300. vm.Set("_sharedspace_getMeta", func(call otto.FunctionCall) otto.Value {
  301. space, ok := getReadableSpace(call)
  302. if !ok {
  303. return otto.NullValue()
  304. }
  305. return jsonReply(space.Metadata())
  306. })
  307. //Invite a user; managers only
  308. vm.Set("_sharedspace_addMember", func(call otto.FunctionCall) otto.Value {
  309. space, ok := getSpace(call)
  310. if !ok {
  311. return otto.FalseValue()
  312. }
  313. target, err := call.Argument(1).ToString()
  314. if err != nil {
  315. return otto.FalseValue()
  316. }
  317. role := optionalString(call, 2)
  318. if role == "" {
  319. role = sharedspace.RoleMember
  320. }
  321. if err := space.AddMember(u.Username, target, role); err != nil {
  322. return otto.FalseValue()
  323. }
  324. return otto.TrueValue()
  325. })
  326. //Remove a member; managers (or the member themselves)
  327. vm.Set("_sharedspace_removeMember", func(call otto.FunctionCall) otto.Value {
  328. space, ok := getSpace(call)
  329. if !ok {
  330. return otto.FalseValue()
  331. }
  332. target, err := call.Argument(1).ToString()
  333. if err != nil {
  334. return otto.FalseValue()
  335. }
  336. if err := space.RemoveMember(u.Username, target); err != nil {
  337. return otto.FalseValue()
  338. }
  339. return otto.TrueValue()
  340. })
  341. //List the members of a space; members and managers only
  342. vm.Set("_sharedspace_listMembers", func(call otto.FunctionCall) otto.Value {
  343. space, ok := getReadableSpace(call)
  344. if !ok {
  345. return otto.NullValue()
  346. }
  347. if _, isMember := space.Role(u.Username); !isMember && !space.CanManage(u.Username) {
  348. return otto.NullValue()
  349. }
  350. return jsonReply(space.Members())
  351. })
  352. //Post a text snippet into a space
  353. vm.Set("_sharedspace_addText", func(call otto.FunctionCall) otto.Value {
  354. space, ok := getSpace(call)
  355. if !ok || !space.CanPost(u.Username) {
  356. return otto.NullValue()
  357. }
  358. text, err := call.Argument(1).ToString()
  359. if err != nil {
  360. return otto.NullValue()
  361. }
  362. item, err := space.AddText(u.Username, text, agiOriginTag)
  363. if err != nil {
  364. return otto.NullValue()
  365. }
  366. val, _ := vm.ToValue(item.ID)
  367. return val
  368. })
  369. //Raise a notification to fellow members of a space through the ArozOS
  370. //notification system. Membership-scoped: only a member (or manager) may
  371. //notify, and only current members can be reached, so this cannot spam
  372. //arbitrary users. Members currently connected to the space's realtime
  373. //channel are skipped (they already receive the live message). Delivery
  374. //per recipient follows that user's own notification preferences (desktop,
  375. //Telegram, email, webhook). Returns the number of members notified, or -1
  376. //on error.
  377. // args: (spaceid, title, [message], [priority], [targetsJSON])
  378. vm.Set("_sharedspace_notifyMembers", func(call otto.FunctionCall) otto.Value {
  379. intReply := func(n int) otto.Value { v, _ := vm.ToValue(n); return v }
  380. space, ok := getSpace(call)
  381. if !ok {
  382. g.RaiseError(errors.New("space not found"))
  383. return intReply(-1)
  384. }
  385. if _, isMember := space.Role(u.Username); !isMember && !space.CanManage(u.Username) {
  386. g.RaiseError(errors.New("permission denied: not a member of this space"))
  387. return intReply(-1)
  388. }
  389. title, err := call.Argument(1).ToString()
  390. if err != nil || title == "undefined" || title == "" {
  391. g.RaiseError(errors.New("notification title cannot be empty"))
  392. return intReply(-1)
  393. }
  394. message := optionalString(call, 2)
  395. priority := optionalString(call, 3)
  396. if priority == "" {
  397. priority = "medium"
  398. }
  399. //An explicit target list (JSON array) narrows the recipients to those
  400. //members; omitting it notifies every other member. A malformed list
  401. //fails safe to "notify nobody".
  402. var requested []string
  403. if targetsJSON := optionalString(call, 4); targetsJSON != "" {
  404. if jerr := json.Unmarshal([]byte(targetsJSON), &requested); jerr != nil {
  405. requested = []string{}
  406. }
  407. }
  408. present := map[string]bool{}
  409. for _, sub := range space.Channel().Subscribers() {
  410. present[sub.Username] = true
  411. }
  412. recipients := resolveNotifyRecipients(space.Members(), present, u.Username, requested)
  413. if len(recipients) == 0 {
  414. //Nobody to notify (everyone is present, or none matched).
  415. return intReply(0)
  416. }
  417. if err := g.buildAndSendNotification(senderLabel, recipients, title, message, priority); err != nil {
  418. g.RaiseError(err)
  419. return intReply(-1)
  420. }
  421. return intReply(len(recipients))
  422. })
  423. //Share a file from the calling user's storage into a space
  424. vm.Set("_sharedspace_addFile", func(call otto.FunctionCall) otto.Value {
  425. space, ok := getSpace(call)
  426. if !ok || !space.CanPost(u.Username) {
  427. return otto.NullValue()
  428. }
  429. vpath, err := call.Argument(1).ToString()
  430. if err != nil {
  431. return otto.NullValue()
  432. }
  433. vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
  434. if !u.CanRead(vpath) {
  435. panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+vpath))
  436. }
  437. fsh, rpath, err := static.VirtualPathToRealPath(vpath, u)
  438. if err != nil {
  439. g.RaiseError(err)
  440. return otto.NullValue()
  441. }
  442. src, err := fsh.FileSystemAbstraction.ReadStream(rpath)
  443. if err != nil {
  444. g.RaiseError(err)
  445. return otto.NullValue()
  446. }
  447. defer src.Close()
  448. name := filepath.Base(vpath)
  449. itemType := sharedspace.ItemTypeFile
  450. if sharedspace.IsImageName(name) {
  451. itemType = sharedspace.ItemTypeImage
  452. }
  453. item, err := space.SaveBlob(itemType, name, u.Username, agiOriginTag, src, g.Option.SharedSpaceManager.MaxUpload())
  454. if err != nil {
  455. g.RaiseError(err)
  456. return otto.NullValue()
  457. }
  458. val, _ := vm.ToValue(item.ID)
  459. return val
  460. })
  461. //List the items in a space
  462. vm.Set("_sharedspace_listItems", func(call otto.FunctionCall) otto.Value {
  463. space, ok := getReadableSpace(call)
  464. if !ok {
  465. return otto.NullValue()
  466. }
  467. items := space.Items()
  468. list := make([]map[string]interface{}, 0, len(items))
  469. for _, item := range items {
  470. list = append(list, agiDescribeItem(item))
  471. }
  472. return jsonReply(list)
  473. })
  474. //Read the content of a text item
  475. vm.Set("_sharedspace_getText", func(call otto.FunctionCall) otto.Value {
  476. space, ok := getReadableSpace(call)
  477. if !ok {
  478. return otto.NullValue()
  479. }
  480. itemID, err := call.Argument(1).ToString()
  481. if err != nil {
  482. return otto.NullValue()
  483. }
  484. item, ok := space.GetItem(itemID)
  485. if !ok || item.Type != sharedspace.ItemTypeText {
  486. return otto.NullValue()
  487. }
  488. val, _ := vm.ToValue(item.Text)
  489. return val
  490. })
  491. //Copy an image / file item into the calling user's storage
  492. vm.Set("_sharedspace_saveFileTo", func(call otto.FunctionCall) otto.Value {
  493. space, ok := getReadableSpace(call)
  494. if !ok {
  495. return otto.FalseValue()
  496. }
  497. itemID, err := call.Argument(1).ToString()
  498. if err != nil {
  499. return otto.FalseValue()
  500. }
  501. item, ok := space.GetItem(itemID)
  502. if !ok || item.DiskPath == "" {
  503. return otto.FalseValue()
  504. }
  505. destVpath, err := call.Argument(2).ToString()
  506. if err != nil {
  507. return otto.FalseValue()
  508. }
  509. destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
  510. if !u.CanWrite(destVpath) {
  511. panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+destVpath))
  512. }
  513. fsh, rpath, err := static.VirtualPathToRealPath(destVpath, u)
  514. if err != nil {
  515. g.RaiseError(err)
  516. return otto.FalseValue()
  517. }
  518. src, err := os.Open(item.DiskPath)
  519. if err != nil {
  520. g.RaiseError(err)
  521. return otto.FalseValue()
  522. }
  523. defer src.Close()
  524. err = fsh.FileSystemAbstraction.WriteStream(rpath, src, 0775)
  525. if err != nil {
  526. g.RaiseError(err)
  527. return otto.FalseValue()
  528. }
  529. return otto.TrueValue()
  530. })
  531. //Remove an item; item uploader, space admin or owner only
  532. vm.Set("_sharedspace_removeItem", func(call otto.FunctionCall) otto.Value {
  533. space, ok := getSpace(call)
  534. if !ok {
  535. return otto.FalseValue()
  536. }
  537. itemID, err := call.Argument(1).ToString()
  538. if err != nil {
  539. return otto.FalseValue()
  540. }
  541. if err := space.RemoveItem(itemID, u.Username); err != nil {
  542. return otto.FalseValue()
  543. }
  544. return otto.TrueValue()
  545. })
  546. //Create a collaborative document
  547. vm.Set("_sharedspace_createDoc", func(call otto.FunctionCall) otto.Value {
  548. space, ok := getSpace(call)
  549. if !ok {
  550. return otto.NullValue()
  551. }
  552. name := optionalString(call, 1)
  553. doc, err := space.CreateDoc(u.Username, name)
  554. if err != nil {
  555. return otto.NullValue()
  556. }
  557. return jsonReply(agiDescribeDoc(doc, true))
  558. })
  559. //List documents (without contents)
  560. vm.Set("_sharedspace_listDocs", func(call otto.FunctionCall) otto.Value {
  561. space, ok := getReadableSpace(call)
  562. if !ok {
  563. return otto.NullValue()
  564. }
  565. docs := space.ListDocs()
  566. list := make([]map[string]interface{}, 0, len(docs))
  567. for _, doc := range docs {
  568. list = append(list, agiDescribeDoc(doc, false))
  569. }
  570. return jsonReply(list)
  571. })
  572. //Fetch one document with content
  573. vm.Set("_sharedspace_getDoc", func(call otto.FunctionCall) otto.Value {
  574. space, ok := getReadableSpace(call)
  575. if !ok {
  576. return otto.NullValue()
  577. }
  578. docID, err := call.Argument(1).ToString()
  579. if err != nil {
  580. return otto.NullValue()
  581. }
  582. doc, ok := space.GetDoc(docID)
  583. if !ok {
  584. return otto.NullValue()
  585. }
  586. return jsonReply(agiDescribeDoc(doc, true))
  587. })
  588. //Compare-and-swap document update
  589. vm.Set("_sharedspace_updateDoc", func(call otto.FunctionCall) otto.Value {
  590. space, ok := getSpace(call)
  591. if !ok {
  592. return otto.NullValue()
  593. }
  594. docID, err := call.Argument(1).ToString()
  595. if err != nil {
  596. return otto.NullValue()
  597. }
  598. baseRev, err := call.Argument(2).ToInteger()
  599. if err != nil {
  600. return otto.NullValue()
  601. }
  602. content, _ := call.Argument(3).ToString()
  603. doc, err := space.UpdateDoc(u.Username, docID, baseRev, content)
  604. if err == sharedspace.ErrRevisionConflict {
  605. current, _ := space.GetDoc(docID)
  606. revision := int64(0)
  607. if current != nil {
  608. revision = current.Revision
  609. }
  610. return jsonReply(map[string]interface{}{
  611. "ok": false,
  612. "conflict": true,
  613. "revision": revision,
  614. })
  615. }
  616. if err != nil {
  617. return otto.NullValue()
  618. }
  619. return jsonReply(map[string]interface{}{
  620. "ok": true,
  621. "revision": doc.Revision,
  622. })
  623. })
  624. //Delete a document; creator or space managers
  625. vm.Set("_sharedspace_deleteDoc", func(call otto.FunctionCall) otto.Value {
  626. space, ok := getSpace(call)
  627. if !ok {
  628. return otto.FalseValue()
  629. }
  630. docID, err := call.Argument(1).ToString()
  631. if err != nil {
  632. return otto.FalseValue()
  633. }
  634. if err := space.DeleteDoc(u.Username, docID); err != nil {
  635. return otto.FalseValue()
  636. }
  637. return otto.TrueValue()
  638. })
  639. //Wrap the native functions into a sharedspace class
  640. vm.Run(`
  641. var sharedspace = {};
  642. sharedspace.createSpace = function(name){ return JSON.parse(_sharedspace_createSpace(name)); };
  643. sharedspace.createSpaceAdvanced = function(name, options){ var r = _sharedspace_createSpaceAdvanced(name, options === undefined ? "" : JSON.stringify(options)); return r === null ? null : JSON.parse(r); };
  644. sharedspace.deleteSpace = _sharedspace_deleteSpace;
  645. sharedspace.listMySpaces = function(){ return JSON.parse(_sharedspace_listMySpaces()); };
  646. sharedspace.listJoinedSpaces = function(){ return JSON.parse(_sharedspace_listJoinedSpaces()); };
  647. sharedspace.listPublicSpaces = function(){ return JSON.parse(_sharedspace_listPublicSpaces()); };
  648. sharedspace.getSpaceInfo = function(spaceid){ return JSON.parse(_sharedspace_getSpaceInfo(spaceid)); };
  649. sharedspace.joinSpace = _sharedspace_joinSpace;
  650. sharedspace.leaveSpace = _sharedspace_leaveSpace;
  651. sharedspace.setAccess = _sharedspace_setAccess;
  652. sharedspace.setMeta = _sharedspace_setMeta;
  653. sharedspace.getMeta = function(spaceid){ var r = _sharedspace_getMeta(spaceid); return r === null ? null : JSON.parse(r); };
  654. sharedspace.addMember = _sharedspace_addMember;
  655. sharedspace.removeMember = _sharedspace_removeMember;
  656. sharedspace.listMembers = function(spaceid){ var r = _sharedspace_listMembers(spaceid); return r === null ? null : JSON.parse(r); };
  657. sharedspace.addText = _sharedspace_addText;
  658. sharedspace.notifyMembers = function(spaceid, title, message, priority, usernames){
  659. return _sharedspace_notifyMembers(spaceid, title,
  660. message === undefined ? "" : message,
  661. priority === undefined ? "" : priority,
  662. (usernames === undefined || usernames === null) ? "" : JSON.stringify(usernames));
  663. };
  664. sharedspace.addFile = _sharedspace_addFile;
  665. sharedspace.listItems = function(spaceid){ var r = _sharedspace_listItems(spaceid); return r === null ? null : JSON.parse(r); };
  666. sharedspace.getText = _sharedspace_getText;
  667. sharedspace.saveFileTo = _sharedspace_saveFileTo;
  668. sharedspace.removeItem = _sharedspace_removeItem;
  669. sharedspace.createDoc = function(spaceid, name){ var r = _sharedspace_createDoc(spaceid, name); return r === null ? null : JSON.parse(r); };
  670. sharedspace.listDocs = function(spaceid){ var r = _sharedspace_listDocs(spaceid); return r === null ? null : JSON.parse(r); };
  671. sharedspace.getDoc = function(spaceid, docid){ var r = _sharedspace_getDoc(spaceid, docid); return r === null ? null : JSON.parse(r); };
  672. sharedspace.updateDoc = function(spaceid, docid, baserev, content){ var r = _sharedspace_updateDoc(spaceid, docid, baserev, content); return r === null ? null : JSON.parse(r); };
  673. sharedspace.deleteDoc = _sharedspace_deleteDoc;
  674. `)
  675. }