A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 680 lines 19 kB view raw
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}