基于 Notion 的 P.A.R.A 实战（二）

自 基于 Notion 的 P.A.R.A 实战 （一） 发布后，这套系统又经过了些小的迭代升级，如今也使用四个月了，运行良好！

和 Obsidian 的使用体验相比较，免去了打开软件，手动同步数据，折腾插件页面等复杂流程；同时 Notion 开放的 api 接口进一步提高了方便程度和可玩性，你完全可以把 Notion 当作一个支持多种视图查看的很好用的数据库； Calendar 的发布也带来了我一直想要拥有的功能：在一个日历中定制自己关心的内容，尽管现在还略显粗糙，但是这就是我想要的功能；搭配浏览器的插件可以把我所有的 **「稍后在读」** 归拢在 Notion 里；手机端也另外写了一些快捷指令来支持对读书笔记的记录，只要复制文本再配合苹果的 「轻点两下」 就可以把内容同步到 Notion 了。

可以说整个使用体验都是很丝滑的，接下来稍微介绍下我都折腾了什么。

一，系统改动

Resource

新增了 Fav 模块，用来记录值得多次阅读的内容。

新增了 Thing 模块，用来记录购买的物品 / 服务的使用体验。

Archive

对内容较多的 Resource 数据表添加周分组。

二，邮件服务 - 同步笔记

邮件服务器

使用 imap 包监听邮件，mailparser 包解析邮件内容

参考代码

Copy
const Imap = require('imap')
const MailParser = require('mailparser').MailParser
const moment = require('moment')
const logger = require('./logger')('sdk/logger')
let emailCount = 10
console.log('-----服务器启动-----' + moment().format('YYYY-MM-DD HH:mm:ss') + '-------------------------------------')

let inflag = false

function mailReceiver(userInfo, mailHandler) {
let imap = new Imap(userInfo)
imap.connect()
setTimeout(() => imap.end(), 5 * 60 * 1000) // 每隔 5 分钟重建连接

imap.once('ready', () => {
setTimeout(() => {
if (!inflag) {
logger.warn(`无法连接，进行重新连接。`)
imap.end()
}
}, 30_000)

imap.openBox('INBOX', false, () => {
inflag = true
imap.once('mail', num => { })
searchUnseen(imap, mailHandler)
})
})

imap.on('mail', num => {
if (num > emailCount) return // 防止大量邮件涌入
searchUnseen(imap, mailHandler)
})

imap.once('error', err => {
logger.error(`链接失败，重新链接 ${err.message}`)
mailReceiver(userInfo, mailHandler)
})

imap.once('end', () => mailReceiver(userInfo, mailHandler))
}

function searchUnseen(imap, mailHandler) {
let timestr = (new Date(+new Date() - (5 * 60 * 1000))).toISOString()
imap.search(['UNSEEN', ['SINCE', timestr]], (err, results) => {
if (err) {
logger.error('search error' + err.mesasge)
imap.end()
return
}
if (results.length == 0) return

try {
let f = imap.fetch(results, { bodies: '', markSeen: true })

f.on('message', (msg, seqno) => {
msg.on('body', (stream, info) => {
let parser = new MailParser()
stream.pipe(parser)
let mail = {}
parser.on("headers", headers => mail.headers = headers)
parser.on("data", content => mail.content = content)
parser.on('end', () => mailHandler(mail))
})

msg.once('end', () => { })
})

f.once('error', err => {
logger.error(`收取邮件报错:${err.message}`)
imap.end()
})

f.once('end', () => imap.end())

} catch (err) {
logger.error('search error' + err.mesasge)
imap.end()
}
})
}

module.exports = mailReceiver

Notion 同步

Notion 的对接分内部集成和外部集成，内部集成较为简单，获取到 token 即可，外部集成则需要实现一整套鉴权，自用采用内部集成的方式就可以了。

详见官方文档：Build your first integration (notion.com)

参考代码

Copy

const { Client } = require("@notionhq/client")

class PARA {
constructor() {
this.client = new Client({
auth: process.env.NOTION_TOKEN, // 申请的 token
})
}

async findPageByName(pageName) {
const res = await this.client.databases.query({
"database_id": this.database_id,
filter: {
property: 'Name',
rich_text: {
equals: pageName
},
},
})
return res.results[0]
}

async addPage(item) {
let { name, mediaType, author, startDate, endDate, remark, score, status, emoji, url } = item
const page = await this.findPageByName(name)
if (page) {
return page
}

const data = {
"parent": { "database_id": this.database_id },
"icon": {
"emoji": emoji || "🥬"
},
"cover": {
"external": {
"url": "https://upload.wikimedia.org/wikipedia/commons/6/62/Tuscankale.jpg"
}
},
"properties": {
}
}
if (name) data.properties.Name = {
"title": [
{
"text": {
"content": name
}
}
]
}
if (author) data.properties.Author = {
"rich_text": [
{
"text": {
"content": author
}
}
]
}
if (remark) data.properties.Remark = {
"rich_text": [
{
"text": {
"content": remark
}
}
]
}
if (status) data.properties.Status = {
"status": {
"name": status
}
}
if (startDate) data.properties.Dates = {
"type": "date",
"date": {
"start": startDate,
}
}
if (endDate) data.properties.Dates = {
"type": "date",
"date": {
"end": endDate
}
}
if (startDate && endDate) data.properties.Dates = {
"type": "date",
"date": {
"start": startDate,
"end": endDate
}
}
if (mediaType) data.properties.MediaType = {
"select": {
"name": mediaType
}
}

if (url) data.properties.Url = {
url
}
if (score) data.properties.Score = { "number": score }
return this.client.pages.create(data)
}

async addBlock(item, content) {
let page = await this.findPageByName(item.name)
if (!page) {
page = await this.addPage({ name: pageName })
}
const res = await this.client.blocks.children.append({
block_id: page.id,
children: [{
"type": "paragraph",
"paragraph": {
"rich_text": [{
"type": "text",
"text": {
"content": content,
"link": null
}
}],
"color": "default"
}
}]
})
return res.results[0]
}
}

class ResouceseArt extends PARA {
constructor() {
super()
this.database_id = '' // 你的数据库 id
}
}

module.exports = {
ResouceseArt
}

**pm2运行服务 **

Copy

/**
* Mail 监听同步到 Notion 数据库
*/
const mailListener = require('../sdk/mail-receiver')
const logger = require('../sdk/logger')('server/mail-sync-notion')
const { ResouceseArt, ResourceFlash, ResourceFav } = require('../sdk/notion-para')
let art = new ResouceseArt()

let userInfo = {
user: 'xxx@outlook.com',
password: '',
host: 'outlook.office365.com',
port: 993,
tls: true
}

mailListener(userInfo, async (mail) => {
if (!mail) return
const subject = mail.headers.get('subject')
const text = mail.content.text
try {
if (subject.indexOf('flash') > -1) {
const page = await flash.addPage({ name: text, url: text })
logger.info('同步成功', { subject, text, page })
}

// 其他处理逻辑
} catch (err) {
logger.error('同步 notion 失败', { err })
}
})

添加快捷指令

先在 iPhone 自带的邮件软件里添加发件人账户，再编写发送邮件的快捷指令，最后在设置 轻点两下 选中创建的快捷指令即可。

借用 2.2 的代码也可以添加 TelegramBot， Cli 等方式将内容同步到 Notion。