const Base = require('./base.js'); const crypto = require('crypto'); const fs = require('fs-extra'); const path = require('path'); const read = require('node-readability'); const cheerio = require('cheerio'); function md5(str) { return crypto.createHash('md5').update(str).digest('hex'); }; module.exports = class extends Base { async __before() { if (['userRegister', 'userLogin', 'noteShare', 'bookmarkDownload', 'hotBookmarks', 'hotBookmarksRandom'].indexOf(this.ctx.action) >= 0) { this.header("Access-Control-Allow-Origin", this.header("origin") || "*"); this.header("Access-Control-Allow-Headers", "x-requested-with"); this.header("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); this.header('Access-Control-Allow-Credentials', true); return; } try { let user = await this.session('user'); if (think.isEmpty(user.id)) { return this.fail(401, '请先登录'); } this.ctx.state.user = user; } catch (error) { // 获取用户的 session 信息,如果为空,返回 false 阻止后续的行为继续执行 return this.fail(401, '请先登录:' + error.toString()); } } indexAction() { return this.display(); } // 注册 async userRegisterAction() { try { let post = this.post(); post.password = md5(post.password); // 进行密码加密 let res = await this.model("users").add(post); this.json({ code: 0, data: res, msg: "注册成功" }); } catch (error) { this.json({ code: 1, data: '', msg: error.toString() }); } } // 登陆 async userLoginAction() { try { let post = this.post(); post.password = md5(post.password); // 进行密码加密 let user = await this.model('users').where({ username: post.username, password: post.password }).find(); if (think.isEmpty(user)) { this.json({ code: 2, msg: "账号或者密码错误" }); } else { delete user.password; const token = await this.session('user', { id: user.id, username: user.username }); user.token = token; await this.model('users').where({ id: user.id }).update({ lastLogin: ['exp', 'NOW()'] }); this.json({ code: 0, data: user, msg: "登陆成功" }); } } catch (error) { this.json({ code: 1, data: '', msg: error.toString() }); } } // 登出 async userLogoutAction() { await this.session(null); this.json({ code: 0, data: '', msg: "退出成功" }); } async userUpdateAction() { let user = this.post(); try { let data = await this.model('users').where({ id: this.ctx.state.user.id }).update(user); this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } async userResetPwdAction() { let old = md5(this.post("old")); let password = md5(this.post("password")); try { let user = await this.model('users').where({ id: this.ctx.state.user.id, password: old }).find(); if (!think.isEmpty(user)) { let data = await this.model('users').where({ id: this.ctx.state.user.id }).update({ password }); this.json({ code: 0, data, msg: "密码更新成功!" }); } else { this.json({ code: 0, data: 0, msg: "旧密码认证失败!" }); } } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 通过session获取自己信息 async userAction() { let full = this.get("full"); let id = this.get("id"); if (full) { let data = await this.model('users').where({ id: id || this.ctx.state.user.id }).find(); delete data.password; this.json({ code: 0, data, msg: '' }); } else { this.json({ code: 0, data: this.ctx.state.user, msg: '' }); } } // 获取分类信息 async tagsAction() { /* // 这里的查询太慢,使用原始的sql查询先替代 let param = this.get(); let tags = await this.model('tags').where({ userId: this.ctx.state.user.id }).order('sort ASC, lastUse DESC').select(); // 这个分类包含的书签与备忘录的个数 for (let tag of tags) { if (param.bookmarkCount) { tag.bookmarkCount = await this.model('bookmarks').where({ tagId: tag.id }).count(); } if (param.noteCount) { tag.noteCount = await this.model('notes').where({ tagId: tag.id }).count(); } } */ let sql = "SELECT t.*, tb.bookmarkCount, tg.noteCount FROM `tags` as t LEFT OUTER JOIN ( SELECT `tagId`, COUNT(tagId) as bookmarkCount FROM bookmarks GROUP BY tagId ) tb ON t.id = tb.tagId LEFT OUTER JOIN ( SELECT `tagId`, COUNT(tagId) as noteCount FROM notes GROUP BY tagId ) tg ON t.id = tg.tagId where t.userId = " + this.ctx.state.user.id; let tags = await this.model('tags').query(sql); this.json({ code: 0, data: tags, msg: '' }); } async tagAddAction() { let name = this.post().name; try { let res = await this.model("tags").add({ userId: this.ctx.state.user.id, name }); this.json({ code: 0, data: res, msg: `分类 ${name} 添加成功` }); } catch (error) { this.json({ code: 1, data: '', msg: error.toString() }); } } // 更新分类 async tagUpdateAction() { let tag = this.post(); try { let data = await this.model('tags').where({ userId: this.ctx.state.user.id, id: tag.id }).update(tag); this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 批量更新排序 async tagSortAction() { let tags = this.post("tags"); try { let data = 0; for (const tag of tags) { let count = await this.model('tags').where({ userId: this.ctx.state.user.id, id: tag.id }).update(tag); data += count; } this.json({ code: 0, data, msg: '分类排序更新成功!' }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 删除分类 async tagDelAction() { let id = this.post("id"); let tagId = id; let userId = this.ctx.state.user.id; try { let data = await this.model("tags").where({ id, userId }).delete(); data = await this.model("bookmarks").where({ tagId, userId }).delete(); this.json({ code: 0, data, msg: `分类删除成功` }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 获取书签 // @todo 如果是自己的任意获取,如果是别人的必须公开才能获取 async bookmarkAction() { let id = this.get("id"); try { let data = await this.model('bookmarks').where({ id }).find(); this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 添加书签 async bookmarkAddAction() { let bookmark = this.post(); bookmark.userId = this.ctx.state.user.id; try { let bookmarkFind = await this.model('bookmarks').where({ userId: this.ctx.state.user.id, url: bookmark.url }).find(); if (!think.isEmpty(bookmarkFind)) { await this.model('bookmarks').where({ userId: this.ctx.state.user.id, id: bookmarkFind.id }).update({ createdAt: ['exp', 'NOW()'] }); this.json({ code: 0, data: bookmarkFind, msg: `书签 ${bookmark.title} 已存在,更新创建日期!` }); return } // 没有分类的直接放未分类里面 if (!bookmark.tagId) { const name = "未分类"; let tag = await this.model("tags").where({ name }).find(); if (!think.isEmpty(tag)) { bookmark.tagId = tag.id; } else { let tagId = await this.model("tags").add({ userId: this.ctx.state.user.id, name }); bookmark.tagId = tagId; } } let data = await this.model("bookmarks").add(bookmark); await this.model('tags').where({ userId: this.ctx.state.user.id, id: bookmark.tagId }).update({ lastUse: think.datetime(new Date()) }); this.json({ code: 0, data, msg: `书签 ${bookmark.title} 添加成功` }); } catch (error) { this.json({ code: 1, data: '', msg: error.toString() }); } } // 删除书签 async bookmarkDelAction() { let bookmark = this.post(); bookmark.userId = this.ctx.state.user.id; try { let data = await this.model("bookmarks").where(bookmark).delete(); this.json({ code: 0, data, msg: `书签删除成功` }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 根据书签id获取书签 async bookmarksByTagAction() { let tagId = this.get("tagId"); let showType = this.get("showType") || "createdAt"; // tagId = -1 个人定制 从自己里面取 let condition = {}; let order = showType + ' DESC'; let page = this.get('page'); let pageSize = parseInt(this.get('pageSize') || 50); if (tagId == -1) { condition = { userId: this.ctx.state.user.id }; } else { condition = { tagId }; } try { // 如果是第0页而且是个人定制的,把 最近点击 与 最近新增 的返回去。 let data = {}; if (page == 0 && tagId == -1) { let count = await this.model('bookmarks').where(condition).count('id'); let totalPages = Math.ceil(count / pageSize); // 按照 2:2:1取数据 let length = Math.ceil(pageSize * 2 / 5); let bookmarks = await this.model('bookmarks').where(condition).order('createdAt DESC').limit(0, length).select(); // 这个取一半 // 取最近点击部分数据 let cnt = 0; let bookmarks2 = await this.model('bookmarks').where(condition).order('lastClick DESC').limit(0, pageSize * 2).select(); // 这个多取一点,有可能跟上面的重复了 for (const bookmark of bookmarks2) { let find = bookmarks.find(item => item.id == bookmark.id); if (!find) { bookmarks.push(bookmark); cnt++; if (cnt >= length) break; } } // 取点击次数最多部分 let bookmarks3 = await this.model('bookmarks').where(condition).order('clickCount DESC').limit(0, pageSize * 2).select(); // 这个多取一点,有可能跟上面的重复了 for (const bookmark of bookmarks3) { let find = bookmarks.find(item => item.id == bookmark.id); if (!find) { bookmarks.push(bookmark); if (bookmarks.length >= pageSize) break; } } data = { count, totalPages, pageSize, data: bookmarks } } else { data = await this.model('bookmarks').where(condition).order(order).page(page, pageSize).countSelect(); } this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } async bookmarksSearchAction() { let condition = {}; let keyword = this.get("keyword"); let tagIds = this.get("tagIds") || []; let range = this.get("range") || "self"; // self hot other let createdAt = this.get("createdAt"); let lastClick = this.get("lastClick"); let tableName = "bookmarks"; if (range == "self") { condition.userId = this.ctx.state.user.id; } else if (range == "hot") { tableName = "hot_bookmarks"; } else if (range == "other") { condition.userId = ['!=', this.ctx.state.user.id]; } if (keyword) { condition._complex = { url: ['like', `%${keyword}%`], title: ['like', `%${keyword}%`], _logic: 'or' } } if (tagIds.length > 0) { condition.tagId = ['IN', tagIds]; } if (createdAt) { condition.createdAt = ['between', createdAt]; } if (lastClick) { condition.lastClick = ['between', lastClick]; } try { let data = await this.model(tableName).where(condition).page(this.get('page') || 1, this.get('pageSize') || 20).countSelect(); if (tableName == "bookmarks") { let ids = []; for (let bookmark of data.data) { ids.push(bookmark.tagId); } let tags = ids.length > 0 ? await this.model('tags').where({ id: ['IN', ids] }).select() : []; for (let bookmark of data.data) { bookmark.tagName = (tags.find((tag) => tag.id == bookmark.tagId) || { name: "未知分类" }).name; } } this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 点击书签 async bookmarkClickAction() { let id = this.post("id"); try { let data = await this.model('bookmarks').where({ userId: this.ctx.state.user.id, id }).update({ clickCount: ['exp', 'clickCount+1'], lastClick: ['exp', 'NOW()'] }); this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 快速跳转到网页 async bookmarShortcutAction() { let url = this.post("url"); try { let bookmark = await this.model('bookmarks').where({ userId: this.ctx.state.user.id, url }).find(); if (!think.isEmpty(bookmark)) { await this.model('bookmarks').where({ userId: this.ctx.state.user.id, id: bookmark.id }).update({ clickCount: ['exp', 'clickCount+1'], lastClick: ['exp', 'NOW()'] }); this.json({ code: 0, data: true }); } else { this.json({ code: 0, data: false }); } } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 更新书签 async bookmarkUpdateAction() { let bookmark = this.post(); try { let data = await this.model('bookmarks').where({ userId: this.ctx.state.user.id, id: bookmark.id }).update(bookmark); this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 书签上传 async bookmarkUploadAction() { // size: 367450, // path: 'C:\\Users\\lucq\\AppData\\Local\\Temp\\upload_4ae3b14dacaa107076d3bddd471ebe39.html', // name: 'exportbookmark-lcq-20200402084709.html', // type: 'text/html', const getRootFolder = function (body) { let h3 = body.find("h3").first(); // let isChrome = typeof h3.attr("personal_toolbar_folder") === "string"; // let isIE = typeof h3.attr("item_id") === "string"; // let isFireFox = h3.text() === "Mozilla Firefox"; let isSafari = typeof h3.attr("folded") === "string"; return isSafari ? body : body.children("dl").first(); }; const parseByString = function (content) { let $ = cheerio.load(content, { decodeEntities: false }); let body = $("body"); let root = []; let rdt = getRootFolder(body).children("dt"); let parseNode = function (node) { let eq0 = node.children().eq(0); let title = eq0.html() || "无标题"; let type = "site"; let href = ""; let attrCreatedAt = ""; let attrLastClick = ""; let attrClickCount = ""; let children = []; switch (eq0[0].name) { case "h3": // folder type = "folder"; let dl = node.children("dl").first(); let dts = dl.children(); let ls = dts.toArray().map(function (ele) { return ele.name !== "dt" ? null : parseNode($(ele)); }); children = ls.filter(function (item) { return item !== null; }); case "a": // site href = eq0.attr("href") || ""; attrCreatedAt = eq0.attr("add_date"); attrLastClick = eq0.attr("last_click"); attrClickCount = eq0.attr("click_count"); } // 处理name if (title.length > 255) { title = title.substring(255); } title = title.replace(/\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/gi, ""); return { title, type, url: href, createdAt: think.datetime(attrCreatedAt ? parseInt(attrCreatedAt) * 1000 : new Date()), lastClick: think.datetime(attrLastClick ? parseInt(attrLastClick) * 1000 : new Date()), clickCount: attrClickCount ? parseInt(attrClickCount) : 1, children: children }; }; rdt.each(function (_, item) { let node = $(item); let child = parseNode(node); root.push(child); }); return root; }; const parseByPath = function (path) { var content = fs.readFileSync(path, 'utf-8'); return parseByString(content); }; const userId = this.ctx.state.user.id; const flatBookmarks = (originBookmarks, tagName, bookmarks) => { for (let bookmark of originBookmarks) { if (bookmark.type == "site") { bookmarks.push({ title: bookmark.title, url: bookmark.url, createdAt: bookmark.createdAt, lastClick: bookmark.lastClick, tagName, clickCount: bookmark.clickCount, userId }); } else if (bookmark.type == "folder") { flatBookmarks(bookmark.children, tagName == '未分类' ? bookmark.title : tagName, bookmarks); } } } let bookmarks = []; const file = this.file("file"); let fileName = 'uploadbookmark-' + this.ctx.state.user.username + '-' + think.datetime(new Date(), "YYYYMMDDHHmmss") + '.html'; if (file) { const filePath = path.join(think.ROOT_PATH, `runtime/upload/${fileName}`); await fs.ensureDir(path.dirname(filePath)); await fs.move(file.path, filePath); let originBookmarks = parseByPath(filePath); Array.isArray(originBookmarks) && originBookmarks.length >= 0 && (originBookmarks[0].title = "未分类"); flatBookmarks(originBookmarks, originBookmarks[0].title, bookmarks); // 传上来的树级目录改为只有一级目录 } let count = 0; let repeat = 0; let tags = await this.model('tags').where({ userId: this.ctx.state.user.id }).select(); for (let bookmark of bookmarks) { let find = await this.model('bookmarks').where({ userId: this.ctx.state.user.id, url: bookmark.url }).find(); if (think.isEmpty(find)) { let tag = tags.find(item => item.name == bookmark.tagName); if (tag) { bookmark.tagId = tag.id; } else { bookmark.tagId = await this.model("tags").add({ userId: this.ctx.state.user.id, name: bookmark.tagName }); tags.push({ id: bookmark.tagId, name: bookmark.tagName }) } delete bookmark.tagName; await this.model("bookmarks").add(bookmark); count++; } else { repeat++; } } this.json({ code: 0, data: count, msg: `书签传入${bookmarks.length}个,重复书签${repeat}个,成功导入${count}个。` }); } // 书签备份 async bookmarkBackupAction() { const sample = ` Bookmarks

Bookmarks

书签栏

JavaScript

Issues · luchenqun/my-bookmark
收件箱 - lcq530485521@gmail.com - Gmail

` let time = (date) => parseInt(new Date(date).getTime() / 1000); // 日期转时间 let now = new Date(); let left = ` Bookmarks

Bookmarks

书签栏

\n`; let middle = ''; let right = `

`; let tags = await this.model('tags').where({ userId: this.ctx.state.user.id }).order('sort ASC, lastUse DESC').select(); for (const tag of tags) { let tagStr = `

${tag.name}

\n

\n`; let bookmarks = await this.model('bookmarks').where({ tagId: tag.id }).select(); for (const bookmark of bookmarks) { tagStr += `

${bookmark.title}\n` } tagStr += `

\n`; middle += bookmarks.length > 0 ? tagStr : ''; } let fileName = 'exportbookmark-' + this.ctx.state.user.username + '-' + think.datetime(new Date(), "YYYYMMDDHHmmss") + '.html'; let filePath = path.join(think.ROOT_PATH, 'runtime', 'backup', fileName); await fs.ensureFile(filePath); await fs.writeFile(filePath, left + middle + right); this.json({ code: 0, data: fileName }); setTimeout(async () => { let exists = await fs.pathExists(filePath); if (exists) { await fs.remove(filePath); } }, 1000 * 60 * 10); // 十分钟内没下载就给删掉 } async bookmarkDownloadAction() { let fileName = this.get('fileName'); let filePath = path.join(think.ROOT_PATH, 'runtime', 'backup', fileName); let exists = await fs.pathExists(filePath); if (exists) { await this.download(filePath); await fs.remove(filePath); } else { this.body = "文件不存在!"; } } // 获取收趣的热门书签 async hotBookmarksAction() { let page = this.get('page'); let pageSize = parseInt(this.get('pageSize') || 50); try { // 如果是第0页而且是个人定制的,把 最近点击 与 最近新增 的返回去。 let data = {}; data = await this.model('hot_bookmarks').order('id DESC').page(page || 1, pageSize).countSelect(); for (let bookmark of data.data) { if (!bookmark.tagName) { bookmark.tagName = "未知"; } } this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 随机获取收趣的热门书签 async hotBookmarksRandomAction() { try { let sql = `SELECT * FROM hot_bookmarks ORDER BY RAND() LIMIT 50;`; let data = await this.model('hot_bookmarks').query(sql); for (let bookmark of data) { if (!bookmark.tagName) { bookmark.tagName = "未知"; } } this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 获取文章 async articleAction() { let url = this.get("url"); async function readArticle(url) { return new Promise(function (resolve, reject) { read(url, (err, article, meta) => { if (err) { reject(err) } else { resolve({ title: article.title }); article.close(); } }); }) } try { let article = await readArticle(url); this.json({ code: 0, data: { title: article.title } }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 新增留言 async adviceAddAction() { let advice = this.post(); advice.userId = this.ctx.state.user.id; if (this.ctx.state.user.username == 'test') { return this.json({ code: 400, data: '', msg: `Test user forbid advice!` }); } try { let res = await this.model("advices").add(advice); this.json({ code: 0, data: res, msg: `留言 添加成功` }); } catch (error) { this.json({ code: 1, data: '', msg: error.toString() }); } } // 获取所有留言 async advicesAction() { try { let data = await this.model("advices").join('users ON users.id = advices.userId').order("createdAt DESC").field('advices.*,users.username').select(); this.json({ code: 0, data }); } catch (error) { this.json({ code: 1, data: '', msg: error.toString() }); } } // 新增 async noteAddAction() { let note = this.post(); note.userId = this.ctx.state.user.id; try { // 没有分类的直接放未分类里面 if (!note.tagId) { const name = "未分类"; let tag = await this.model("tags").where({ name }).find(); if (!think.isEmpty(tag)) { note.tagId = tag.id; } else { let tagId = await this.model("tags").add({ userId: this.ctx.state.user.id, name }); note.tagId = tagId; } } let data = await this.model("notes").add(note); this.json({ code: 0, data, msg: `备忘添加成功` }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 更新备忘 async noteUpdateAction() { let note = this.post(); try { let data = await this.model('notes').where({ userId: this.ctx.state.user.id, id: note.id }).update(note); this.json({ code: 0, data, msg: `备忘更新成功` }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } // 更新 async noteDelAction() { let note = this.post(); note.userId = this.ctx.state.user.id; try { let data = await this.model("notes").where(note).delete(); this.json({ code: 0, data, msg: `备忘删除成功` }); } catch (error) { this.json({ code: 1, msg: error.toString() }); } } async notesAction() { let params = this.get(); let where = {}; try { if (params.keyword) { where.content = ['like', `%${params.keyword}%`] } if (params.tagId) { where.tagId = params.tagId; } where.userId = this.ctx.state.user.id; let data = await this.model('notes').where(where).order("createdAt DESC").page(this.get('page'), this.get('pageSize')).countSelect(); this.json({ code: 0, data }) } catch (error) { this.json({ code: 1, msg: error.toString() }); } } async noteShareAction() { let id = this.get("id"); let json = this.get("json"); let note = await this.model('notes').where({ id, public: 1 }).find(); if (json) { this.json(JSON.parse(note.content)) } else { let body = think.isEmpty(note) ? "备忘为非公开或者已删除!" : note.content; this.body = `

\n\n${body}\n\n
`; } } };