A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package api
18
19import (
20 "net/http"
21 "strings"
22 "time"
23
24 "github.com/88250/gulu"
25 "github.com/gin-gonic/gin"
26 "github.com/siyuan-note/siyuan/kernel/model"
27 "github.com/siyuan-note/siyuan/kernel/treenode"
28 "github.com/siyuan-note/siyuan/kernel/util"
29)
30
31func getNotebookInfo(c *gin.Context) {
32 ret := gulu.Ret.NewResult()
33 defer c.JSON(http.StatusOK, ret)
34
35 arg, ok := util.JsonArg(c, ret)
36 if !ok {
37 return
38 }
39
40 boxID := arg["notebook"].(string)
41 if util.InvalidIDPattern(boxID, ret) {
42 return
43 }
44
45 box := model.Conf.Box(boxID)
46 if nil == box {
47 ret.Code = -1
48 ret.Msg = "notebook [" + boxID + "] not found"
49 return
50 }
51
52 boxInfo := box.GetInfo()
53 ret.Data = map[string]interface{}{
54 "boxInfo": boxInfo,
55 }
56}
57
58func setNotebookIcon(c *gin.Context) {
59 ret := gulu.Ret.NewResult()
60 defer c.JSON(http.StatusOK, ret)
61
62 arg, ok := util.JsonArg(c, ret)
63 if !ok {
64 return
65 }
66
67 boxID := arg["notebook"].(string)
68 icon := arg["icon"].(string)
69 model.SetBoxIcon(boxID, icon)
70}
71
72func changeSortNotebook(c *gin.Context) {
73 ret := gulu.Ret.NewResult()
74 defer c.JSON(http.StatusOK, ret)
75
76 arg, ok := util.JsonArg(c, ret)
77 if !ok {
78 return
79 }
80
81 idsArg := arg["notebooks"].([]interface{})
82 var ids []string
83 for _, p := range idsArg {
84 ids = append(ids, p.(string))
85 }
86 model.ChangeBoxSort(ids)
87}
88
89func renameNotebook(c *gin.Context) {
90 ret := gulu.Ret.NewResult()
91 defer c.JSON(http.StatusOK, ret)
92
93 arg, ok := util.JsonArg(c, ret)
94 if !ok {
95 return
96 }
97
98 notebook := arg["notebook"].(string)
99 if util.InvalidIDPattern(notebook, ret) {
100 return
101 }
102
103 name := arg["name"].(string)
104 err := model.RenameBox(notebook, name)
105 if err != nil {
106 ret.Code = -1
107 ret.Msg = err.Error()
108 ret.Data = map[string]interface{}{"closeTimeout": 5000}
109 return
110 }
111
112 evt := util.NewCmdResult("renamenotebook", 0, util.PushModeBroadcast)
113 evt.Data = map[string]interface{}{
114 "box": notebook,
115 "name": name,
116 }
117 util.PushEvent(evt)
118}
119
120func removeNotebook(c *gin.Context) {
121 ret := gulu.Ret.NewResult()
122 defer c.JSON(http.StatusOK, ret)
123
124 arg, ok := util.JsonArg(c, ret)
125 if !ok {
126 return
127 }
128
129 notebook := arg["notebook"].(string)
130 if util.InvalidIDPattern(notebook, ret) {
131 return
132 }
133
134 if util.ReadOnly && !model.IsUserGuide(notebook) {
135 ret.Code = -1
136 ret.Msg = model.Conf.Language(34)
137 ret.Data = map[string]interface{}{"closeTimeout": 5000}
138 return
139 }
140
141 err := model.RemoveBox(notebook)
142 if err != nil {
143 ret.Code = -1
144 ret.Msg = err.Error()
145 return
146 }
147
148 evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast)
149 evt.Data = map[string]interface{}{
150 "box": notebook,
151 }
152 evt.Callback = arg["callback"]
153 util.PushEvent(evt)
154}
155
156func createNotebook(c *gin.Context) {
157 ret := gulu.Ret.NewResult()
158 defer c.JSON(http.StatusOK, ret)
159
160 arg, ok := util.JsonArg(c, ret)
161 if !ok {
162 return
163 }
164
165 name := arg["name"].(string)
166 id, err := model.CreateBox(name)
167 if err != nil {
168 ret.Code = -1
169 ret.Msg = err.Error()
170 return
171 }
172
173 existed, err := model.Mount(id)
174 if err != nil {
175 ret.Code = -1
176 ret.Msg = err.Error()
177 return
178 }
179
180 box := model.Conf.Box(id)
181 if nil == box {
182 ret.Code = -1
183 ret.Msg = "opened notebook [" + id + "] not found"
184 return
185 }
186
187 ret.Data = map[string]interface{}{
188 "notebook": box,
189 }
190
191 evt := util.NewCmdResult("createnotebook", 0, util.PushModeBroadcast)
192 evt.Data = map[string]interface{}{
193 "box": box,
194 "existed": existed,
195 }
196 util.PushEvent(evt)
197}
198
199func openNotebook(c *gin.Context) {
200 ret := gulu.Ret.NewResult()
201 defer c.JSON(http.StatusOK, ret)
202
203 arg, ok := util.JsonArg(c, ret)
204 if !ok {
205 return
206 }
207
208 notebook := arg["notebook"].(string)
209 if util.InvalidIDPattern(notebook, ret) {
210 return
211 }
212
213 isUserGuide := model.IsUserGuide(notebook)
214 if util.ReadOnly && !isUserGuide {
215 ret.Code = -1
216 ret.Msg = model.Conf.Language(34)
217 ret.Data = map[string]interface{}{"closeTimeout": 5000}
218 return
219 }
220
221 if isUserGuide && util.ContainerIOS == util.Container {
222 // iOS 端不再支持打开用户指南,请参考桌面端用户指南
223 // 用户指南中包含了付费相关内容,无法通过商店上架审核
224 // Opening the user guide is no longer supported on iOS https://github.com/siyuan-note/siyuan/issues/11492
225 ret.Code = -1
226 ret.Msg = model.Conf.Language(215)
227 ret.Data = map[string]interface{}{"closeTimeout": 7000}
228 return
229 }
230
231 msgId := util.PushMsg(model.Conf.Language(45), 1000*60*15)
232 defer util.PushClearMsg(msgId)
233 existed, err := model.Mount(notebook)
234 if err != nil {
235 ret.Code = -1
236 ret.Msg = err.Error()
237 return
238 }
239
240 box := model.Conf.Box(notebook)
241 if nil == box {
242 ret.Code = -1
243 ret.Msg = "opened notebook [" + notebook + "] not found"
244 return
245 }
246
247 evt := util.NewCmdResult("mount", 0, util.PushModeBroadcast)
248 evt.Data = map[string]interface{}{
249 "box": box,
250 "existed": existed,
251 }
252 evt.Callback = arg["callback"]
253 util.PushEvent(evt)
254
255 if isUserGuide {
256 appArg := arg["app"]
257 app := ""
258 if nil != appArg {
259 app = appArg.(string)
260 }
261
262 go func() {
263 var startID string
264 i := 0
265 for ; i < 70; i++ {
266 time.Sleep(100 * time.Millisecond)
267 guideStartID := map[string]string{
268 "20210808180117-czj9bvb": "20200812220555-lj3enxa",
269 "20211226090932-5lcq56f": "20211226115423-d5z1joq",
270 "20210808180117-6v0mkxr": "20200923234011-ieuun1p",
271 "20240530133126-axarxgx": "20240530101000-4qitucx",
272 }
273 startID = guideStartID[notebook]
274 if treenode.ExistBlockTree(startID) {
275 util.BroadcastByTypeAndApp("main", app, "openFileById", 0, "", map[string]interface{}{
276 "id": startID,
277 })
278 break
279 }
280 }
281 }()
282 }
283}
284
285func closeNotebook(c *gin.Context) {
286 ret := gulu.Ret.NewResult()
287 defer c.JSON(http.StatusOK, ret)
288
289 arg, ok := util.JsonArg(c, ret)
290 if !ok {
291 return
292 }
293
294 notebook := arg["notebook"].(string)
295 if util.InvalidIDPattern(notebook, ret) {
296 return
297 }
298 model.Unmount(notebook)
299}
300
301func getNotebookConf(c *gin.Context) {
302 ret := gulu.Ret.NewResult()
303 defer c.JSON(http.StatusOK, ret)
304
305 arg, ok := util.JsonArg(c, ret)
306 if !ok {
307 return
308 }
309
310 notebook := arg["notebook"].(string)
311 if util.InvalidIDPattern(notebook, ret) {
312 return
313 }
314
315 box := model.Conf.GetBox(notebook)
316 if nil == box {
317 ret.Code = -1
318 ret.Msg = "notebook [" + notebook + "] not found"
319 return
320 }
321
322 ret.Data = map[string]interface{}{
323 "box": box.ID,
324 "name": box.Name,
325 "conf": box.GetConf(),
326 }
327}
328
329func setNotebookConf(c *gin.Context) {
330 ret := gulu.Ret.NewResult()
331 defer c.JSON(http.StatusOK, ret)
332
333 arg, ok := util.JsonArg(c, ret)
334 if !ok {
335 return
336 }
337
338 notebook := arg["notebook"].(string)
339 if util.InvalidIDPattern(notebook, ret) {
340 return
341 }
342
343 box := model.Conf.GetBox(notebook)
344 if nil == box {
345 ret.Code = -1
346 ret.Msg = "notebook [" + notebook + "] not found"
347 return
348 }
349
350 param, err := gulu.JSON.MarshalJSON(arg["conf"])
351 if err != nil {
352 ret.Code = -1
353 ret.Msg = err.Error()
354 return
355 }
356
357 boxConf := box.GetConf()
358 if err = gulu.JSON.UnmarshalJSON(param, boxConf); err != nil {
359 ret.Code = -1
360 ret.Msg = err.Error()
361 return
362 }
363
364 boxConf.RefCreateSavePath = util.TrimSpaceInPath(boxConf.RefCreateSavePath)
365 if "" != boxConf.RefCreateSavePath {
366 if !strings.HasSuffix(boxConf.RefCreateSavePath, "/") {
367 boxConf.RefCreateSavePath += "/"
368 }
369 }
370
371 boxConf.DailyNoteSavePath = util.TrimSpaceInPath(boxConf.DailyNoteSavePath)
372 if "" != boxConf.DailyNoteSavePath {
373 if !strings.HasPrefix(boxConf.DailyNoteSavePath, "/") {
374 boxConf.DailyNoteSavePath = "/" + boxConf.DailyNoteSavePath
375 }
376 }
377 if "/" == boxConf.DailyNoteSavePath {
378 ret.Code = -1
379 ret.Msg = model.Conf.Language(49)
380 return
381 }
382
383 boxConf.DailyNoteTemplatePath = util.TrimSpaceInPath(boxConf.DailyNoteTemplatePath)
384 if "" != boxConf.DailyNoteTemplatePath {
385 if !strings.HasSuffix(boxConf.DailyNoteTemplatePath, ".md") {
386 boxConf.DailyNoteTemplatePath += ".md"
387 }
388 if !strings.HasPrefix(boxConf.DailyNoteTemplatePath, "/") {
389 boxConf.DailyNoteTemplatePath = "/" + boxConf.DailyNoteTemplatePath
390 }
391 }
392
393 boxConf.DocCreateSavePath = util.TrimSpaceInPath(boxConf.DocCreateSavePath)
394
395 box.SaveConf(boxConf)
396 ret.Data = boxConf
397}
398
399func lsNotebooks(c *gin.Context) {
400 ret := gulu.Ret.NewResult()
401 defer c.JSON(http.StatusOK, ret)
402
403 flashcard := false
404
405 // 兼容旧版接口,不能直接使用 util.JsonArg()
406 arg := map[string]interface{}{}
407 if err := c.ShouldBindJSON(&arg); err == nil {
408 if arg["flashcard"] != nil {
409 flashcard = arg["flashcard"].(bool)
410 }
411 }
412
413 var notebooks []*model.Box
414 if flashcard {
415 notebooks = model.GetFlashcardNotebooks()
416 } else {
417 var err error
418 notebooks, err = model.ListNotebooks()
419 if err != nil {
420 return
421 }
422 }
423
424 ret.Data = map[string]interface{}{
425 "notebooks": notebooks,
426 }
427}