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 model
18
19import (
20 "encoding/hex"
21 "errors"
22 "fmt"
23 "net/http"
24 "os"
25 "path/filepath"
26 "regexp"
27 "strconv"
28 "strings"
29 "time"
30
31 "github.com/88250/gulu"
32 "github.com/88250/lute/parse"
33 "github.com/siyuan-note/httpclient"
34 "github.com/siyuan-note/logging"
35 "github.com/siyuan-note/siyuan/kernel/conf"
36 "github.com/siyuan-note/siyuan/kernel/task"
37 "github.com/siyuan-note/siyuan/kernel/util"
38)
39
40var ErrFailedToConnectCloudServer = errors.New("failed to connect cloud server")
41
42func CloudChatGPT(msg string, contextMsgs []string) (ret string, stop bool, err error) {
43 if nil == Conf.GetUser() {
44 return
45 }
46
47 payload := map[string]interface{}{}
48 var messages []map[string]interface{}
49 for _, contextMsg := range contextMsgs {
50 messages = append(messages, map[string]interface{}{
51 "role": "user",
52 "content": contextMsg,
53 })
54 }
55 messages = append(messages, map[string]interface{}{
56 "role": "user",
57 "content": msg,
58 })
59 payload["messages"] = messages
60
61 requestResult := gulu.Ret.NewResult()
62 request := httpclient.NewCloudRequest30s()
63 _, err = request.
64 SetSuccessResult(requestResult).
65 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
66 SetBody(payload).
67 Post(util.GetCloudServer() + "/apis/siyuan/ai/chatGPT")
68 if err != nil {
69 logging.LogErrorf("chat gpt failed: %s", err)
70 err = ErrFailedToConnectCloudServer
71 return
72 }
73 if 0 != requestResult.Code {
74 err = errors.New(requestResult.Msg)
75 stop = true
76 return
77 }
78
79 data := requestResult.Data.(map[string]interface{})
80 choices := data["choices"].([]interface{})
81 if 1 > len(choices) {
82 stop = true
83 return
84 }
85 choice := choices[0].(map[string]interface{})
86 message := choice["message"].(map[string]interface{})
87 ret = message["content"].(string)
88
89 if nil != choice["finish_reason"] {
90 finishReason := choice["finish_reason"].(string)
91 if "length" == finishReason {
92 stop = false
93 } else {
94 stop = true
95 }
96 } else {
97 stop = true
98 }
99 return
100}
101
102func StartFreeTrial() (err error) {
103 if nil == Conf.GetUser() {
104 return errors.New(Conf.Language(31))
105 }
106
107 requestResult := gulu.Ret.NewResult()
108 request := httpclient.NewCloudRequest30s()
109 resp, err := request.
110 SetSuccessResult(requestResult).
111 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
112 Post(util.GetCloudServer() + "/apis/siyuan/user/startFreeTrial")
113 if err != nil {
114 logging.LogErrorf("start free trial failed: %s", err)
115 return ErrFailedToConnectCloudServer
116 }
117 if http.StatusOK != resp.StatusCode {
118 logging.LogErrorf("start free trial failed: %d", resp.StatusCode)
119 return ErrFailedToConnectCloudServer
120 }
121 if 0 != requestResult.Code {
122 return errors.New(requestResult.Msg)
123 }
124 return
125}
126
127func DeactivateUser() (err error) {
128 requestResult := gulu.Ret.NewResult()
129 request := httpclient.NewCloudRequest30s()
130 resp, err := request.
131 SetSuccessResult(requestResult).
132 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
133 Post(util.GetCloudServer() + "/apis/siyuan/user/deactivate")
134 if err != nil {
135 logging.LogErrorf("deactivate user failed: %s", err)
136 return ErrFailedToConnectCloudServer
137 }
138
139 if 401 == resp.StatusCode {
140 err = errors.New(Conf.Language(31))
141 return
142 }
143
144 if 0 != requestResult.Code {
145 logging.LogErrorf("deactivate user failed: %s", requestResult.Msg)
146 return errors.New(requestResult.Msg)
147 }
148 return
149}
150
151func SetCloudBlockReminder(id, data string, timed int64) (err error) {
152 requestResult := gulu.Ret.NewResult()
153 payload := map[string]interface{}{"dataId": id, "data": data, "timed": timed}
154 request := httpclient.NewCloudRequest30s()
155 resp, err := request.
156 SetSuccessResult(requestResult).
157 SetBody(payload).
158 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
159 Post(util.GetCloudServer() + "/apis/siyuan/calendar/setBlockReminder")
160 if err != nil {
161 logging.LogErrorf("set block reminder failed: %s", err)
162 return ErrFailedToConnectCloudServer
163 }
164
165 if 401 == resp.StatusCode {
166 err = errors.New(Conf.Language(31))
167 return
168 }
169
170 if 0 != requestResult.Code {
171 logging.LogErrorf("set block reminder failed: %s", requestResult.Msg)
172 return errors.New(requestResult.Msg)
173 }
174 return
175}
176
177var uploadToken = ""
178var uploadTokenTime int64
179
180func LoadUploadToken() (err error) {
181 now := time.Now().Unix()
182 if 3600 >= now-uploadTokenTime {
183 return
184 }
185
186 requestResult := gulu.Ret.NewResult()
187 request := httpclient.NewCloudRequest30s()
188 resp, err := request.
189 SetSuccessResult(requestResult).
190 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
191 Post(util.GetCloudServer() + "/apis/siyuan/upload/token")
192 if err != nil {
193 logging.LogErrorf("get upload token failed: %s", err)
194 return ErrFailedToConnectCloudServer
195 }
196
197 if 401 == resp.StatusCode {
198 err = errors.New(Conf.Language(31))
199 return
200 }
201
202 if 0 != requestResult.Code {
203 logging.LogErrorf("get upload token failed: %s", requestResult.Msg)
204 return
205 }
206
207 resultData := requestResult.Data.(map[string]interface{})
208 uploadToken = resultData["uploadToken"].(string)
209 uploadTokenTime = now
210 return
211}
212
213var (
214 subscriptionExpirationReminded bool
215)
216
217func RefreshCheckJob() {
218 go refreshSubscriptionExpirationRemind()
219 go refreshUser()
220 go refreshAnnouncement()
221 go refreshCheckDownloadInstallPkg()
222}
223
224func refreshSubscriptionExpirationRemind() {
225 if subscriptionExpirationReminded {
226 return
227 }
228 subscriptionExpirationReminded = true
229
230 if "ios" == util.Container {
231 return
232 }
233
234 defer logging.Recover()
235
236 if IsSubscriber() && -1 != Conf.GetUser().UserSiYuanProExpireTime {
237 expired := int64(Conf.GetUser().UserSiYuanProExpireTime)
238 now := time.Now().UnixMilli()
239 if now >= expired { // 已经过期
240 if now-expired <= 1000*60*60*24*2 { // 2 天内提醒 https://github.com/siyuan-note/siyuan/issues/7816
241 task.AppendAsyncTaskWithDelay(task.PushMsg, 30*time.Second, util.PushErrMsg, Conf.Language(128), 0)
242 }
243 return
244 }
245 remains := int((expired - now) / 1000 / 60 / 60 / 24)
246 expireDay := 15 // 付费订阅提前 15 天提醒
247 if 2 == Conf.GetUser().UserSiYuanSubscriptionPlan {
248 expireDay = 3 // 试用订阅提前 3 天提醒
249 }
250
251 if 0 < remains && expireDay > remains {
252 task.AppendAsyncTaskWithDelay(task.PushMsg, 7*time.Second, util.PushErrMsg, fmt.Sprintf(Conf.Language(127), remains), 0)
253 return
254 }
255 }
256}
257
258func refreshUser() {
259 defer logging.Recover()
260
261 if nil != Conf.GetUser() {
262 time.Sleep(2 * time.Minute)
263 if nil != Conf.GetUser() {
264 RefreshUser(Conf.GetUser().UserToken)
265 }
266 subscriptionExpirationReminded = false
267 }
268}
269
270func refreshCheckDownloadInstallPkg() {
271 defer logging.Recover()
272
273 time.Sleep(3 * time.Minute)
274 checkDownloadInstallPkg()
275 if "" != getNewVerInstallPkgPath() {
276 util.PushMsg(Conf.Language(62), 15*1000)
277 }
278}
279
280func refreshAnnouncement() {
281 defer logging.Recover()
282
283 time.Sleep(1 * time.Minute)
284 announcementConf := filepath.Join(util.HomeDir, ".config", "siyuan", "announcement.json")
285 var existingAnnouncements, newAnnouncements []*Announcement
286 if gulu.File.IsExist(announcementConf) {
287 data, err := os.ReadFile(announcementConf)
288 if err != nil {
289 logging.LogErrorf("read announcement conf failed: %s", err)
290 return
291 }
292 if err = gulu.JSON.UnmarshalJSON(data, &existingAnnouncements); err != nil {
293 logging.LogErrorf("unmarshal announcement conf failed: %s", err)
294 os.Remove(announcementConf)
295 return
296 }
297 }
298
299 for _, announcement := range getAnnouncements() {
300 var exist bool
301 for _, existingAnnouncement := range existingAnnouncements {
302 if announcement.Id == existingAnnouncement.Id {
303 exist = true
304 break
305 }
306 }
307 if !exist {
308 existingAnnouncements = append(existingAnnouncements, announcement)
309 if Conf.CloudRegion == announcement.Region {
310 newAnnouncements = append(newAnnouncements, announcement)
311 }
312 }
313 }
314
315 data, err := gulu.JSON.MarshalJSON(existingAnnouncements)
316 if err != nil {
317 logging.LogErrorf("marshal announcement conf failed: %s", err)
318 return
319 }
320 if err = os.WriteFile(announcementConf, data, 0644); err != nil {
321 logging.LogErrorf("write announcement conf failed: %s", err)
322 return
323 }
324
325 for _, newAnnouncement := range newAnnouncements {
326 util.PushMsg(fmt.Sprintf(Conf.Language(11), newAnnouncement.URL, newAnnouncement.Title), 0)
327 }
328}
329
330func RefreshUser(token string) {
331 threeDaysAfter := util.CurrentTimeMillis() + 1000*60*60*24*3
332 if "" == token {
333 if "" != Conf.UserData {
334 Conf.SetUser(loadUserFromConf())
335 }
336 if nil == Conf.GetUser() {
337 return
338 }
339
340 var tokenExpireTime int64
341 tokenExpireTime, err := strconv.ParseInt(Conf.GetUser().UserTokenExpireTime+"000", 10, 64)
342 if err != nil {
343 logging.LogErrorf("convert token expire time [%s] failed: %s", Conf.GetUser().UserTokenExpireTime, err)
344 util.PushErrMsg(Conf.Language(19), 5000)
345 return
346 }
347
348 if threeDaysAfter > tokenExpireTime {
349 token = Conf.GetUser().UserToken
350 goto Net
351 }
352 return
353 }
354
355Net:
356 start := time.Now()
357 user, err := getUser(token)
358 if err != nil {
359 if nil == Conf.GetUser() || errInvalidUser == err {
360 util.PushErrMsg(Conf.Language(19), 5000)
361 return
362 }
363
364 var tokenExpireTime int64
365 tokenExpireTime, err = strconv.ParseInt(Conf.GetUser().UserTokenExpireTime+"000", 10, 64)
366 if err != nil {
367 logging.LogErrorf("convert token expire time [%s] failed: %s", Conf.GetUser().UserTokenExpireTime, err)
368 util.PushErrMsg(Conf.Language(19), 5000)
369 return
370 }
371
372 if threeDaysAfter > tokenExpireTime {
373 util.PushErrMsg(Conf.Language(19), 5000)
374 return
375 }
376 return
377 }
378
379 Conf.SetUser(user)
380 data, _ := gulu.JSON.MarshalJSON(user)
381 Conf.UserData = util.AESEncrypt(string(data))
382 Conf.Save()
383
384 if elapsed := time.Now().Sub(start).Milliseconds(); 3000 < elapsed {
385 logging.LogInfof("get cloud user elapsed [%dms]", elapsed)
386 }
387 return
388}
389
390func loadUserFromConf() *conf.User {
391 if "" == Conf.UserData {
392 return nil
393 }
394
395 data := util.AESDecrypt(Conf.UserData)
396 data, _ = hex.DecodeString(string(data))
397 user := &conf.User{}
398 if err := gulu.JSON.UnmarshalJSON(data, &user); err == nil {
399 return user
400 }
401 return nil
402}
403
404func RemoveCloudShorthands(ids []string) (err error) {
405 result := map[string]interface{}{}
406 request := httpclient.NewCloudRequest30s()
407 body := map[string]interface{}{
408 "ids": ids,
409 }
410 resp, err := request.
411 SetSuccessResult(&result).
412 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
413 SetBody(body).
414 Post(util.GetCloudServer() + "/apis/siyuan/inbox/removeCloudShorthands")
415 if err != nil {
416 logging.LogErrorf("remove cloud shorthands failed: %s", err)
417 err = ErrFailedToConnectCloudServer
418 return
419 }
420
421 if 401 == resp.StatusCode {
422 err = errors.New(Conf.Language(31))
423 return
424 }
425
426 code := result["code"].(float64)
427 if 0 != code {
428 logging.LogErrorf("remove cloud shorthands failed: %s", result["msg"])
429 err = errors.New(result["msg"].(string))
430 return
431 }
432 return
433}
434
435func GetCloudShorthand(id string) (ret map[string]interface{}, err error) {
436 result := map[string]interface{}{}
437 request := httpclient.NewCloudRequest30s()
438 resp, err := request.
439 SetSuccessResult(&result).
440 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
441 Post(util.GetCloudServer() + "/apis/siyuan/inbox/getCloudShorthand?id=" + id)
442 if err != nil {
443 logging.LogErrorf("get cloud shorthand failed: %s", err)
444 err = ErrFailedToConnectCloudServer
445 return
446 }
447
448 if 401 == resp.StatusCode {
449 err = errors.New(Conf.Language(31))
450 return
451 }
452
453 code := result["code"].(float64)
454 if 0 != code {
455 logging.LogErrorf("get cloud shorthand failed: %s", result["msg"])
456 err = errors.New(result["msg"].(string))
457 return
458 }
459 ret = result["data"].(map[string]interface{})
460 t, _ := strconv.ParseInt(id, 10, 64)
461 hCreated := util.Millisecond2Time(t)
462 ret["hCreated"] = hCreated.Format("2006-01-02 15:04")
463
464 md := ret["shorthandContent"].(string)
465 ret["shorthandMd"] = md
466
467 luteEngine := NewLute()
468 luteEngine.SetFootnotes(true)
469 tree := parse.Parse("", []byte(md), luteEngine.ParseOptions)
470 luteEngine.RenderOptions.ProtyleMarkNetImg = false
471 content := luteEngine.ProtylePreview(tree, luteEngine.RenderOptions)
472 ret["shorthandContent"] = content
473 return
474}
475
476func GetCloudShorthands(page int) (result map[string]interface{}, err error) {
477 result = map[string]interface{}{}
478 request := httpclient.NewCloudRequest30s()
479 resp, err := request.
480 SetSuccessResult(&result).
481 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
482 Post(util.GetCloudServer() + "/apis/siyuan/inbox/getCloudShorthands?p=" + strconv.Itoa(page))
483 if err != nil {
484 logging.LogErrorf("get cloud shorthands failed: %s", err)
485 err = ErrFailedToConnectCloudServer
486 return
487 }
488
489 if 401 == resp.StatusCode {
490 err = errors.New(Conf.Language(31))
491 return
492 }
493
494 code := result["code"].(float64)
495 if 0 != code {
496 logging.LogErrorf("get cloud shorthands failed: %s", result["msg"])
497 err = errors.New(result["msg"].(string))
498 return
499 }
500
501 luteEngine := NewLute()
502 audioRegexp := regexp.MustCompile("<audio.*>.*</audio>")
503 videoRegexp := regexp.MustCompile("<video.*>.*</video>")
504 fileRegexp := regexp.MustCompile("\\[文件]\\(.*\\)")
505 shorthands := result["data"].(map[string]interface{})["shorthands"].([]interface{})
506 for _, item := range shorthands {
507 shorthand := item.(map[string]interface{})
508 id := shorthand["oId"].(string)
509 t, _ := strconv.ParseInt(id, 10, 64)
510 hCreated := util.Millisecond2Time(t)
511 shorthand["hCreated"] = hCreated.Format("2006-01-02 15:04")
512
513 desc := shorthand["shorthandDesc"].(string)
514 desc = audioRegexp.ReplaceAllString(desc, " 语音 ")
515 desc = videoRegexp.ReplaceAllString(desc, " 视频 ")
516 desc = fileRegexp.ReplaceAllString(desc, " 文件 ")
517 desc = strings.ReplaceAll(desc, "\n\n", "")
518 desc = strings.TrimSpace(desc)
519 shorthand["shorthandDesc"] = desc
520
521 md := shorthand["shorthandContent"].(string)
522 shorthand["shorthandMd"] = md
523 tree := parse.Parse("", []byte(md), luteEngine.ParseOptions)
524 luteEngine.RenderOptions.ProtyleMarkNetImg = false
525 content := luteEngine.ProtylePreview(tree, luteEngine.RenderOptions)
526 shorthand["shorthandContent"] = content
527 }
528 return
529}
530
531var errInvalidUser = errors.New("invalid user")
532
533func getUser(token string) (*conf.User, error) {
534 result := map[string]interface{}{}
535 request := httpclient.NewCloudRequest30s()
536 resp, err := request.
537 SetSuccessResult(&result).
538 SetBody(map[string]string{"token": token}).
539 Post(util.GetCloudServer() + "/apis/siyuan/user")
540 if err != nil {
541 logging.LogErrorf("get community user failed: %s", err)
542 return nil, errors.New(Conf.Language(18))
543 }
544 if http.StatusOK != resp.StatusCode {
545 logging.LogErrorf("get community user failed: %d", resp.StatusCode)
546 return nil, errors.New(Conf.Language(18))
547 }
548
549 code := result["code"].(float64)
550 if 0 != code {
551 if 255 == code {
552 return nil, errInvalidUser
553 }
554 logging.LogErrorf("get community user failed: %s", result["msg"])
555 return nil, errors.New(Conf.Language(18))
556 }
557
558 dataStr := result["data"].(string)
559 data := util.AESDecrypt(dataStr)
560 user := &conf.User{}
561 if err = gulu.JSON.UnmarshalJSON(data, &user); err != nil {
562 logging.LogErrorf("get community user failed: %s", err)
563 return nil, errors.New(Conf.Language(18))
564 }
565 return user, nil
566}
567
568func UseActivationcode(code string) (err error) {
569 code = strings.TrimSpace(code)
570 code = util.RemoveInvalid(code)
571 requestResult := gulu.Ret.NewResult()
572 request := httpclient.NewCloudRequest30s()
573 resp, err := request.
574 SetSuccessResult(requestResult).
575 SetBody(map[string]string{"data": code}).
576 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
577 Post(util.GetCloudServer() + "/apis/siyuan/useActivationcode")
578 if err != nil {
579 logging.LogErrorf("check activation code failed: %s", err)
580 return ErrFailedToConnectCloudServer
581 }
582 if http.StatusOK != resp.StatusCode {
583 logging.LogErrorf("check activation code failed: %d", resp.StatusCode)
584 return ErrFailedToConnectCloudServer
585 }
586 if 0 != requestResult.Code {
587 return errors.New(requestResult.Msg)
588 }
589 return
590}
591
592func CheckActivationcode(code string) (retCode int, msg string) {
593 code = strings.TrimSpace(code)
594 code = util.RemoveInvalid(code)
595 retCode = 1
596 requestResult := gulu.Ret.NewResult()
597 request := httpclient.NewCloudRequest30s()
598 resp, err := request.
599 SetSuccessResult(requestResult).
600 SetBody(map[string]string{"data": code}).
601 SetCookies(&http.Cookie{Name: "symphony", Value: Conf.GetUser().UserToken}).
602 Post(util.GetCloudServer() + "/apis/siyuan/checkActivationcode")
603 if err != nil {
604 logging.LogErrorf("check activation code failed: %s", err)
605 msg = ErrFailedToConnectCloudServer.Error()
606 return
607 }
608 if http.StatusOK != resp.StatusCode {
609 logging.LogErrorf("check activation code failed: %d", resp.StatusCode)
610 msg = ErrFailedToConnectCloudServer.Error()
611 return
612 }
613 if 0 == requestResult.Code {
614 retCode = 0
615 }
616 msg = requestResult.Msg
617 return
618}
619
620func Login(userName, password, captcha string, cloudRegion int) (ret *gulu.Result) {
621 Conf.CloudRegion = cloudRegion
622 Conf.Save()
623 util.CurrentCloudRegion = cloudRegion
624
625 result := map[string]interface{}{}
626 request := httpclient.NewCloudRequest30s()
627 resp, err := request.
628 SetSuccessResult(&result).
629 SetBody(map[string]string{"userName": userName, "userPassword": password, "captcha": captcha}).
630 Post(util.GetCloudServer() + "/apis/siyuan/login")
631 if err != nil {
632 logging.LogErrorf("login failed: %s", err)
633 ret = gulu.Ret.NewResult()
634 ret.Code = -1
635 ret.Msg = Conf.Language(18) + ": " + err.Error()
636 return
637 }
638 if http.StatusOK != resp.StatusCode {
639 logging.LogErrorf("login failed: %d", resp.StatusCode)
640 ret = gulu.Ret.NewResult()
641 ret.Code = -1
642 ret.Msg = Conf.Language(18)
643 return
644 }
645
646 ret = &gulu.Result{
647 Code: int(result["code"].(float64)),
648 Msg: result["msg"].(string),
649 Data: map[string]interface{}{
650 "userName": result["userName"],
651 "token": result["token"],
652 "needCaptcha": result["needCaptcha"],
653 },
654 }
655 if -1 == ret.Code {
656 ret.Code = 1
657 }
658 return
659}
660
661func Login2fa(token, code string) (map[string]interface{}, error) {
662 result := map[string]interface{}{}
663 request := httpclient.NewCloudRequest30s()
664 _, err := request.
665 SetSuccessResult(&result).
666 SetBody(map[string]string{"twofactorAuthCode": code}).
667 SetHeader("token", token).
668 Post(util.GetCloudServer() + "/apis/siyuan/login/2fa")
669 if err != nil {
670 logging.LogErrorf("login 2fa failed: %s", err)
671 return nil, errors.New(Conf.Language(18))
672 }
673 return result, nil
674}
675
676func LogoutUser() {
677 Conf.UserData = ""
678 Conf.SetUser(nil)
679 Conf.Save()
680}