使用koa2+wechaty打造我的微信小祕書

前言

開篇連連問?css

  • 你是否是有閒置的微信號?
  • 你想不想有個小祕書定時提醒你將要作的事情?
  • 你是否爲忘記一些記念日而懊惱?
  • 你是否加班到很晚,而忘記了今天和別人有約?
  • 你是否是下班還記得拿快遞,到家後才發現忘記了?
  • 你是否是想學習一下如何作一個微信小祕書?

若是以上問題你有一條符合的話,那就能夠安心讀下去了,由於微信小祕書能夠幫你解決大部分的問題。固然沒有符合的話,也能夠繼續讀下去,由於既然來了就說明你仍是有興趣的😆。html

固然小祕書不符合你要求的話《用Node+wechaty寫一個爬蟲腳本天天定時給女(男)朋友發微信暖心話》也能夠看一下奧!node

技術棧

node: 建議最新穩定版,因爲wechaty依賴,因此至少node > 10以上版本 Koa: web開發框架,用來編寫服務端應用程序git

MongoDB:非關係數據庫github

mongoose:鏈接 mongodb的一個庫web

wechaty:提供網頁版微信相關操做api接口mongodb

node-schedule - 定時任務數據庫

項目地址

github:github.com/gengchen528…express

功能

很聽你話的私人小祕書,幫你建立定時任務,每日提醒,記念日提醒,當日提醒npm

文字支持格式:(關鍵詞之間須要用空格分開,特別是暱稱和時間要分隔開才行的,時間是24小時制,暫時還不支持表情😭)

  • 「提醒 我 18:30 快要下班了,準備一下,不要忘記帶東西」 (當天指定時間提醒)
  • 「提醒 其餘人暱稱 2019-09-10 10:00 工做再忙,也要記得喝水」(委託小祕書提醒其餘人)
  • 「提醒 我 天天 8:00 出門記得帶鑰匙,公交卡還有飯盒」(每日指定時間提醒)
  • 「提醒 wo 2019-09-10 10:00 還有兩天就是女友的生日,要提早準備一下」 (指定日期時間提醒)

效果圖以下:

提醒本身

委託提醒(前提是你和你想要提醒的人都是小祕書的好友,採用的是暱稱查找用戶,不是備註要注意)

數據庫中已添加任務

準備工做

因爲本項目使用了MongoDB數據庫,因此須要在電腦或服務器中自行安裝,這裏就不在贅述安裝過程啦,不懂怎麼安裝的小夥伴能夠戳連接,MongoDB的可視化工具,我在Mac上使用的是mongobooster,有須要的小夥伴能夠去下載一下。

代碼說明

目錄結構

  • config: koa,定時任務器,superagent的配置目錄
  • mongodb: mongodb鏈接相關配置文件,schema設計與model的主要目錄
  • untils: 抽取的共用方法

wechaty核心代碼

index.js

微信登陸,定時任務初始化,小祕書具體實現的主要文件。接口getScheduleList在每次登錄後會從數據庫拉取未執行的定時任務並進行初始化,防止意外掉線後沒法恢復定時任務。同時每次設置定時任務,接口addSchedule會直接向數據庫中插入一條任務記錄並把任務添加到定時任務列表中。每次任務執行完畢後,接口updateSchedule都會更新數據庫中指定任務的狀態,防止任務重複執行。

const { Wechaty, Friendship } = require('wechaty')
const schedule = require('./config/schedule')
const { FileBox } = require('file-box')
const Qrterminal = require('qrcode-terminal')
const { request } = require('./config/superagent')
const untils = require('./untils/index')
const host = 'http://127.0.0.1:3008/api'

// 每次登陸初始化定時任務
initSchedule = async(list) => {
    try {
        for (item of list) {
            let time = item.isLoop ? item.time : new Date(item.time)
            schedule.setSchedule(time, async() => {
                let contact = await bot.Contact.find({ name: item.subscribe })
                console.log('你的專屬提醒開啓啦!')
                await contact.say(item.content)
                if (!item.isLoop) {
                    request(host + '/updateSchedule', 'POST', '', { id: item._id }).then((result) => {
                        console.log('更新定時任務成功')
                    }).catch(err => {
                        console.log('更新錯誤', err)
                    })
                }
            })
        }
    } catch (err) {
        console.log('初始化定時任務失敗', err)
    }
}

// 二維碼生成
onScan = (qrcode, status) => {
    Qrterminal.generate(qrcode)
    const qrImgUrl = ['https://api.qrserver.com/v1/create-qr-code/?data=', encodeURIComponent(qrcode)].join('')
    console.log(qrImgUrl)
}

// 登陸事件
onLogin = async(user) => {
    console.log(`貼心助理${user}登陸了`)
    request(host + '/getScheduleList', 'GET').then((res) => {
        let text = JSON.parse(res.text)
        let scheduleList = text.data
        console.log('定時任務列表', scheduleList)
        initSchedule(scheduleList)
    }).catch(err => {
        console.log('獲取任務列表錯誤', err)
    })
}

// 登出事件
onLogout = (user) => {
    console.log(`${user} 登出了`)
}

// 消息監聽
onMessage = async(msg) => {
    const contact = msg.from()
    const content = msg.text()
    const room = msg.room()
    if (msg.self()) return
    if (room) {
        const roomName = await room.topiac()
        console.log(`羣名: ${roomName} 發消息人: ${contact.name()} 內容: ${content}`)
    } else {
        console.log(`發消息人: ${contact.name()} 消息內容: ${content}`)

        let keywordArray = content.replace(/\s+/g, ' ').split(" ") // 把多個空格替換成一個空格,並使用空格做爲標記,拆分關鍵詞
        console.log("分詞後效果", keywordArray)
        if (keywordArray[0] === "提醒") {
            let scheduleObj = untils.contentDistinguish(contact, keywordArray)
            addSchedule(scheduleObj)
            contact.say('小助手已經把你的提醒牢記在小本本上了')
        } else {
            contact.say('很高興你能和我聊天,來試試個人新功能吧!回覆案例:「提醒 我 18:30 下班回家」,建立你的專屬提醒,記得關鍵詞之間使用空格分隔開')
        }
    }
}

// 添加定時提醒
addSchedule = async(obj) => {
    request(host + '/addSchedule', 'POST', '', obj).then(async(res) => {
        res = JSON.parse(res.text)
        let nickName = res.data.subscribe
        let time = res.data.time
        let Rule1 = res.data.isLoop ? time : new Date(time)
        let content = res.data.content
        let contact = await bot.Contact.find({ name: nickName })
        schedule.setSchedule(Rule1, async() => {
            console.log('你的專屬提醒開啓啦!')
            await contact.say(content)
            if (!res.isLoop) {
                request(host + '/updateSchedule', 'POST', '', { id: res.data._id }).then((result) => {
                    console.log('更新定時任務成功')
                }).catch(err => {
                    console.log('更新錯誤', err)
                })
            }
        })
    }).catch(err => {
        console.log('錯誤', err)
    })
}

// 自動加好友
onFriendShip = async(friendship) => {
    let logMsg
    try {
        logMsg = '添加好友' + friendship.contact().name()
        console.log(logMsg)
        switch (friendship.type()) {
            /**
             *
             * 1. New Friend Request
             *
             * when request is set, we can get verify message from `request.hello`,
             * and accept this request by `request.accept()`
             */
            case Friendship.Type.Receive:
                await friendship.accept()
                break
                /**
                 *
                 * 2. Friend Ship Confirmed
                 *
                 */
            case Friendship.Type.Confirm:
                logMsg = 'friend ship confirmed with ' + friendship.contact().name()
                break
        }
    } catch (e) {
        logMsg = e.message
    }
    console.log(logMsg)
}
const bot = new Wechaty({ name: 'WechatEveryDay' })
bot.on('scan', onScan)
bot.on('login', onLogin)
bot.on('logout', onLogout)
bot.on('message', onMessage)
bot.on('friendship', onFriendShip)
bot.start()
    .then(() => { console.log('開始登錄微信') })
    .catch(e => console.error(e))
複製代碼

untils/index.js

這裏主要是輸入關鍵詞後的處理方法,在index.js中,我把用戶輸入的關鍵詞根據空格來進行分詞處理,放到一個數組中,而後傳入contentDistinguish()方法中。根據關鍵詞的不一樣來進行處理是不是屬於每日任務,當日定時任務,仍是屬於指定日期任。由於不一樣的定時任務類型,在時間格式上是有所區分的,每日任務我採用的是Cron風格定時器,相似0 30 8 * * *(天天8點30提醒)這種,而指定日期時間和當日任務我使用的是new Date('2019-9-10 12:30:00')來處理。 同時爲了兼容性,在日期處理上採用了全角替換半角的冒號格式,內容上爲了更符合祕書的身份,將主語我所有替換成你,也處理了本身給本身建立定時任務與你給別人建立定時任務內容上的不一樣。

getToday = () => { // 獲取今天日期
    const date = new Date()
    let year = date.getFullYear()
    let month = date.getMonth() + 1
    let day = date.getDate()
    return year + '-' + month + '-' + day + ' '
}

convertTime = (time) => { // 轉換定時格式
    let array = time.split(':')
    return "0 " + array[1] + ' ' + array[0] + ' * * *'
}

contentDistinguish = (contact, keywordArray) => {
    let scheduleObj = {}
    let today = getToday()
    scheduleObj.setter = contact.name() // 設置定時任務的用戶
    scheduleObj.subscribe = (keywordArray[1] === "我") ? contact.name() : keywordArray[1] // 定時任務接收者
    if (keywordArray[2] === "天天") { // 判斷是否屬於循環任務
        console.log('已設置每日定時任務')
        scheduleObj.isLoop = true
        scheduleObj.time = convertTime(keywordArray[3])
        scheduleObj.content = (scheduleObj.setter === scheduleObj.subscribe) ? scheduleObj.content = "親愛的" + scheduleObj.subscribe + ",舒適提醒:" + keywordArray[4].replace('我', '你') : "親愛的" + scheduleObj.subscribe + "," + scheduleObj.setter + "委託我提醒你," + keywordArray[4].replace('我', '你')
    } else if (keywordArray[2] && keywordArray[2].indexOf('-') > -1) {
        console.log('已設置指定日期時間任務')
        scheduleObj.isLoop = false
        scheduleObj.time = keywordArray[2] + ' ' + keywordArray[3].replace(':', ':')
        scheduleObj.content = (scheduleObj.setter === scheduleObj.subscribe) ? scheduleObj.content = "親愛的" + scheduleObj.subscribe + ",舒適提醒:" + keywordArray[4].replace('我', '你') : "親愛的" + scheduleObj.subscribe + "," + scheduleObj.setter + "委託我提醒你," + keywordArray[4].replace('我', '你')
    } else {
        console.log('已設置當天任務')
        scheduleObj.isLoop = false
        scheduleObj.time = today + keywordArray[2].replace(':', ':')
        scheduleObj.content = (scheduleObj.setter === scheduleObj.subscribe) ? scheduleObj.content = "親愛的" + scheduleObj.subscribe + ",舒適提醒:" + keywordArray[3].replace('我', '你') : "親愛的" + scheduleObj.subscribe + "," + scheduleObj.setter + "委託我提醒你," + keywordArray[3].replace('我', '你')
    }
    return scheduleObj
}
module.exports = {
    getToday,
    convertTime,
    contentDistinguish
}
複製代碼

koa核心代碼

koa就不用多說了,TJ大神繼express後的又一神做,很輕量,並且擺脫了「回調地獄」的問題,放一張大神頭像鎮樓(髮型很帥啊,哈哈)

koa服務默認使用3008端口,若是修改的話,須要在index.js中修改host的地址。這裏目前寫了三個接口,分別是添加定時任務,獲取定時任務列表和更新任務列表,對應的數據庫curd操做均在mongodb/model.js中完成

config/koa.js

const Koa = require("koa")
const Router = require("koa-router")
const bodyParser = require('koa-bodyparser')
const model = require("../mongodb/model")

const app = new Koa()
const router = new Router()
app.use(bodyParser())

router.post('/api/addSchedule', async(ctx, next) => { // 添加定時任務
    let body = ctx.request.body;
    console.log('接收參數', body)
    let res = await model.insert(body);
    ctx.body = { code: 200, msg: "ok", data: res }
    next()
})

router.get('/api/getScheduleList', async(ctx, next) => { // 獲取定時任務列表
    const condition = { hasExpired: false }
    let res = await model.find(condition)
    ctx.response.status = 200;
    ctx.body = { code: 200, msg: "ok", data: res }
    next()
})
router.post('/api/updateSchedule', async(ctx, next) => { // 更新定時任務
    const condition = { _id: ctx.request.body.id }
    let res = await model.update(condition)
    ctx.response.status = 200;
    ctx.body = { code: 200, msg: "ok", data: res }
    next()
})

const handler = async(ctx, next) => {
    try {
        await next();
    } catch (err) {
        ctx.respose.status = err.statusCode || err.status || 500;
        ctx.response.type = 'html';
        ctx.response.body = '<p>出錯啦</p>';
        ctx.app.emit('error', err, ctx);
    }
}

app.use(handler)
app.on('error', (err) => {
    console.error('server error:', err)
})

app.use(router.routes())
app.use(router.allowedMethods())
app.listen(3008, () => {
    console.log('[demo] route-use-middleware is starting at port 3008')
})
複製代碼

mongose核心代碼

mongodb/config.js

這裏主要是MongoDB的主要配置文件,使用了mongoose連接MongoDB數據庫,默認端口27017,建立了一個名爲wechatAssitant的庫

const mongoose = require("mongoose")

const db_url = 'mongodb://localhost:27017/wechatAssitant'
let db = mongoose.connect(db_url, { useNewUrlParser: true })

//鏈接成功
mongoose.connection.on('connect', () => {
    console.log("Mongoose connection open to " + db_url)
})

//鏈接異常
mongoose.connection.on('error', (err) => {
    console.log("Mongoose connection erro " + err);
});

//鏈接斷開
mongoose.connection.on('disconnected', () => {
    console.log("Mongoose connection disconnected ");
});

module.exports = mongoose
複製代碼

mongodb/schema.js

在Mongoose裏一切都是從Schema開始的,每個Schema都會映射到MongoDB的一個collection上。Schema定義了collection裏documents的模板(或者說是框架),以下代碼定義了定時任務的Schema:

const mongoose = require('./config')
const Schema = mongoose.Schema

let assistant = new Schema({
    subscribe: String, // 訂閱者
    setter: String, // 設定任務者
    content: String, // 訂閱內容
    time: String, // 定時日期
    isLoop: Boolean, // 是否爲循環定時任務
    hasExpired: { type: Boolean, default: false }, // 判斷任務是否過時
    createdAt: { type: Date, default: Date.now },
})

module.exports = mongoose.model('Assistant', assistant)
複製代碼

mongodb/model.js

爲了使用定義好的Schema,咱們須要把Schema轉換成咱們可使用的model(實際上是把Schema編譯成model,因此對於Schema的一切定義都要在compile以前完成)。也就是說model纔是咱們能夠進行操做的handle,具體實現代碼mongoose.model('Assistant', assistant),這裏咱們已經在schema.js文件中直接導出,直接在model.js中引用

const Assistant = require('./schema')

module.exports = {
    insert: (conditions) => { // 添加定時任務
        return new Promise((resolve, reject) => {
            Assistant.create(conditions, (err, doc) => {
                if (err) return reject(err)
                console.log('建立成功', doc)
                return resolve(doc)
            })
        })
    },

    find: (conditions) => { // 獲取定時任務列表
        return new Promise((resolve, reject) => {
            Assistant.find(conditions, (err, doc) => {
                if (err) return reject(err)
                return resolve(doc)
            })
        })
    },
    update: (conditions) => { // 更新定時任務狀態
        return new Promise((resolve, reject) => {
            Assistant.updateOne(conditions, { hasExpired: true }, (err, doc) => {
                if (err) return reject(err)
                return resolve(doc)
            })
        })
    }
}
複製代碼

項目運行

因爲須要安裝chromium,因此要先配置一下鏡像,注意因爲wechaty的限制,必須使用node10以上版本

npm或者yarn 配置淘寶源

(很重要,防止下載chromium失敗,由於下載文件在150M左右,因此在執行npm run start後須要等待下載大概一兩分鐘以上,請耐心等待) npm

npm config set registry https://registry.npm.taobao.org
npm config set disturl https://npm.taobao.org/dist
npm config set puppeteer_download_host https://npm.taobao.org/mirrors
複製代碼

yarn

yarn config set registry https://registry.npm.taobao.org
yarn config set disturl https://npm.taobao.org/dist
yarn config set puppeteer_download_host https://npm.taobao.org/mirrors
複製代碼

下載項目安裝依賴

git clone git@github.com:gengchen528/wechat-assistant.git
cd wechat-assistant.git
npm install
npm run start
複製代碼

掃描登陸

用微信掃描控制檯顯示的二維碼,在手機上贊成登陸便可。使用其餘微信發送指定格式文字進行添加定時任務。

服務器部署

一、若是須要在服務器中部署,須要先掃描二維碼登陸一次,生成微信維持登陸狀態的json文件,以下圖:

二、生成此文件後,可使用pm2工具進行進程守護。因爲爲了方便,本地開發的時候,我設置的 npm run start同時執行了兩條命令,因此在服務器端部署的時候,建議先啓動 koa.js後再啓動 index.js

常見問題

  1. 個人微信號沒法登錄

    從2017年6月下旬開始,使用基於web版微信接入方案存在大機率的被限制登錄的可能性。 主要表現爲:沒法登錄Web 微信,但不影響手機等其餘平臺。 驗證是否被限制登錄: wx.qq.com 上掃碼查看是否能登錄。 更多內容詳見:

    Can not login with error message: 當前登陸環境異常。爲了你的賬號安全,暫時不能登陸web微信。

    [謠言] 微信將會關閉網頁版本

    新註冊的微信號沒法登錄

  2. 執行npm run start時沒法安裝puppet-puppeteer&&Chromium

    • Centos7下部署出現如下問題

      問題緣由:segmentfault.com/a/119000001…

      解決方案:

      #依賴庫
        yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y
      
        #字體
        yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc -y
      複製代碼
    • windows下,下載puppeteer失敗

      連接:pan.baidu.com/s/1YF09nELp… 提取碼:0mrz

      把下載的文件放到以下圖路徑,並解壓到當前文件夾中便可

  3. 支持 紅包、轉帳、朋友圈… 嗎

    支付相關 - 紅包、轉帳、收款 等都不支持

  4. 更多關於wechaty功能相關接口

    參考wechaty官網文檔

  5. 其餘問題解決方案

    • 本地是否安裝了mongodb數據庫
    • 先檢查node版本是否大於10
    • 確認npm或yarn已經配置好淘寶源
    • 存在package-lock.json文件先刪除
    • 刪除node_modules後從新執行npm installcnpm install

注意

本項目屬於我的興趣開發,開源出來是爲了技術交流,請勿使用此項目作違反微信規定或者其餘違法事情。 建議使用小號進行測試,有被微信封禁網頁端登陸權限的風險(客戶端不受影響),請確保自願使用。

最後

個人小祕書已經學會了自動加好友功能,因此有興趣的小夥伴能夠加個人微信進行測試,她也能夠是你的私人小祕書😆(注意別發太多信息,會把她玩壞的)

趕快親自試一試吧,相信你會挖掘出更多好玩的功能

github:github.com/gengchen528…

另外個人公衆號已經接入微軟小冰,關注後發語音會有小姐姐的聲音陪你聊天,也能夠和她文字聊天,有興趣能夠試試看,單身的歡迎來撩

相關文章
相關標籤/搜索