diff --git a/lib/api_material.js b/lib/api_material.js index a5151bc..4f621bb 100644 --- a/lib/api_material.js +++ b/lib/api_material.js @@ -5,7 +5,7 @@ const path = require('path'); const { promisify } = require('util'); const { stat } = require('fs'); const statAsync = promisify(stat); - +const fetch = require('node-fetch'); const formstream = require('formstream'); const { postJSON } = require('./util'); @@ -29,22 +29,22 @@ const { postJSON } = require('./util'); */ exports.uploadMaterial = async function (filepath, type) { const { accessToken } = await this.ensureAccessToken(); - var stat = await statAsync(filepath); - var form = formstream(); + const stat = await statAsync(filepath); + const form = formstream(); form.file('media', filepath, path.basename(filepath), stat.size); - var url = this.prefix + 'material/add_material?access_token=' + accessToken + '&type=' + type; - var opts = { + const url = this.prefix + 'material/add_material?access_token=' + accessToken + '&type=' + type; + const opts = { dataType: 'json', method: 'POST', timeout: 60000, // 60秒超时 headers: form.headers(), - data: form + data: form, }; return this.request(url, opts); }; ['image', 'voice', 'thumb'].forEach(function (type) { - var method = 'upload' + type[0].toUpperCase() + type.substring(1) + 'Material'; + const method = 'upload' + type[0].toUpperCase() + type.substring(1) + 'Material'; exports[method] = async function (filepath) { return this.uploadMaterial(filepath, type); }; @@ -71,17 +71,17 @@ exports.uploadMaterial = async function (filepath, type) { */ exports.uploadVideoMaterial = async function (filepath, description) { const { accessToken } = await this.ensureAccessToken(); - var stat = await statAsync(filepath); - var form = formstream(); + const stat = await statAsync(filepath); + const form = formstream(); form.file('media', filepath, path.basename(filepath), stat.size); form.field('description', JSON.stringify(description)); - var url = this.prefix + 'material/add_material?access_token=' + accessToken + '&type=video'; - var opts = { + const url = this.prefix + 'material/add_material?access_token=' + accessToken + '&type=video'; + const opts = { dataType: 'json', method: 'POST', timeout: 60000, // 60秒超时 headers: form.headers(), - data: form + data: form, }; return this.request(url, opts); }; @@ -111,15 +111,65 @@ exports.uploadVideoMaterial = async function (filepath, description) { * * Result: * ``` - * {"errcode":0,"errmsg":"ok"} + * { + * "media_id":MEDIA_ID + * } * ``` * @param {Object} news 图文对象 */ exports.uploadNewsMaterial = async function (news) { const { accessToken } = await this.ensureAccessToken(); - var url = this.prefix + 'material/add_news?access_token=' + accessToken; + const url = this.prefix + 'material/add_news?access_token=' + accessToken; return this.request(url, postJSON(news)); }; + + +/** + * 新建草稿 + * 详情请见: + * ``` + * { + * "articles": [ + * { + * "title":TITLE, + * "author":AUTHOR, + * "digest":DIGEST, + * "content":CONTENT, + * "content_source_url":CONTENT_SOURCE_URL, + * "thumb_media_id":THUMB_MEDIA_ID, + * "need_open_comment": 0, + * "only_fans_can_comment": 0 + * } + * //若新增的是多图文素材,则此处应还有几段articles结构 + * ] + * } + * ``` + * Examples: + * ``` + * api.addDraft(news); + * ``` + * + * Result: + * ``` + * { + * "media_id":MEDIA_ID + * } + * ``` + * @param {Object} news 图文对象 + */ +exports.addDraft = async function (news) { + const { accessToken } = await this.ensureAccessToken(); + const url = this.prefix + 'draft/add?access_token=' + accessToken; + return fetch(url, { + method: 'POST', + body: JSON.stringify(news), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + }).then(res => res.json()); +}; + /** * 更新永久图文素材 * News: @@ -153,9 +203,49 @@ exports.uploadNewsMaterial = async function (news) { */ exports.updateNewsMaterial = async function (news) { const { accessToken } = await this.ensureAccessToken(); - var url = this.prefix + 'material/update_news?access_token=' + accessToken; + const url = this.prefix + 'material/update_news?access_token=' + accessToken; return this.request(url, postJSON(news)); }; + +/** + * 修改草稿 + * News: + * ``` + * { + * "media_id":MEDIA_ID, + * "index":INDEX, + * "articles": { + * "title":TITLE, + * "author":AUTHOR, + * "digest":DIGEST, + * "content":CONTENT, + * "content_source_url":CONTENT_SOURCE_URL, + * "thumb_media_id":THUMB_MEDIA_ID, + * "show_cover_pic": 1, + * "need_open_comment": 0, + * "only_fans_can_comment": 0 + * } + * } + * ``` + * Examples: + * ``` + * api.updateDraft(news); + * ``` + * Result: + * ``` + * { + * "errcode":ERRCODE, + * "errmsg": "ERRMSG" + * } + * ``` + * @param {Object} news 图文对象 + */ +exports.updateDraft = async function (news) { + const { accessToken } = await this.ensureAccessToken(); + const url = this.prefix + 'draft/update?access_token=' + accessToken; + return this.request(url, postJSON(news)); +}; + /** * 根据媒体ID获取永久素材 * 详情请见: @@ -169,14 +259,58 @@ exports.updateNewsMaterial = async function (news) { */ exports.getMaterial = async function (mediaId) { const { accessToken } = await this.ensureAccessToken(); - var url = this.prefix + 'material/get_material?access_token=' + accessToken; - var opts = { + const url = this.prefix + 'material/get_material?access_token=' + accessToken; + const opts = { method: 'POST', - data: JSON.stringify({'media_id': mediaId}), + data: JSON.stringify({ 'media_id': mediaId }), headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, - timeout : 60000 // 60秒超时 + timeout: 60000, // 60秒超时 + }; + return this.request(url, opts); +}; + +/** + * 获取草稿 + * 详情请见: + * Examples: + * ``` + * api.getDraft('media_id'); + * ``` + * + * - `result` + * ``` + * { + * "news_item": [ + * { + * "title":TITLE, + * "author":AUTHOR, + * "digest":DIGEST, + * "content":CONTENT, + * "content_source_url":CONTENT_SOURCE_URL, + * "thumb_media_id":THUMB_MEDIA_ID, + * "show_cover_pic": 1, + * "need_open_comment": 0, + * "only_fans_can_comment": 0, + * "url":URL + * } + * //多图文消息应有多段 news_item 结构 + * ] + * } + * ``` + * @param {String} mediaId 媒体文件的ID + */ +exports.getDraft = async function (mediaId) { + const { accessToken } = await this.ensureAccessToken(); + const url = this.prefix + 'draft/get?access_token=' + accessToken; + const opts = { + method: 'POST', + data: JSON.stringify({ 'media_id': mediaId }), + headers: { + 'Content-Type': 'application/json', + }, + timeout: 60000, // 60秒超时 }; return this.request(url, opts); }; @@ -194,8 +328,31 @@ exports.getMaterial = async function (mediaId) { */ exports.removeMaterial = async function (mediaId) { const { accessToken } = await this.ensureAccessToken(); - var url = this.prefix + 'material/del_material?access_token=' + accessToken; - return this.request(url, postJSON({'media_id': mediaId})); + const url = this.prefix + 'material/del_material?access_token=' + accessToken; + return this.request(url, postJSON({ 'media_id': mediaId })); +}; + +/** + * 删除草稿 + * 详情请见: + * Examples: + * ``` + * api.removeMaterial('media_id'); + * ``` + * + * - `result`, 调用正常时得到的文件Buffer对象 + * ``` + * { + * "errcode":ERRCODE, + * "errmsg":"ERRMSG" + * } + * ``` + * * @param {String} mediaId 媒体文件的ID + */ +exports.deleteDraft = async function (mediaId) { + const { accessToken } = await this.ensureAccessToken(); + const url = this.prefix + 'draft/delete?access_token=' + accessToken; + return this.request(url, postJSON({ 'media_id': mediaId })); }; /** @@ -219,8 +376,29 @@ exports.removeMaterial = async function (mediaId) { */ exports.getMaterialCount = async function () { const { accessToken } = await this.ensureAccessToken(); - var url = this.prefix + 'material/get_materialcount?access_token=' + accessToken; - return this.request(url, {dataType: 'json'}); + const url = this.prefix + 'material/get_materialcount?access_token=' + accessToken; + return this.request(url, { dataType: 'json' }); +}; + +/** + * 获取草稿总数 + * 详情请见: + * Examples: + * ``` + * api.getDraftCount(); + * ``` + * + * - `res`, HTTP响应对象 * Result: + * ``` + * { + * "total_count":TOTAL_COUNT + * } + * ``` + */ +exports.getDraftCount = async function () { + const { accessToken } = await this.ensureAccessToken(); + const url = this.prefix + 'draft/count?access_token=' + accessToken; + return this.request(url, { dataType: 'json' }); }; /** @@ -252,11 +430,71 @@ exports.getMaterialCount = async function () { */ exports.getMaterials = async function (type, offset, count) { const { accessToken } = await this.ensureAccessToken(); - var url = this.prefix + 'material/batchget_material?access_token=' + accessToken; - var data = { + const url = this.prefix + 'material/batchget_material?access_token=' + accessToken; + const data = { type: type, offset: offset, - count: count + count: count, + }; + return this.request(url, postJSON(data)); +}; + +/** + * 获取草稿列表 + * 详情请见: + * Examples: + * ``` + * { + * "offset":OFFSET, + * "count":COUNT, + * "no_content":NO_CONTENT + * } + * ``` + * + * ``` + * api.getDrafts(offset, count, no_content); + * ``` + * + * - `result`, 调用正常时得到的文件Buffer对象 + * - `res`, HTTP响应对象 * Result: + * ``` + * { + * "total_count":TOTAL_COUNT, + * "item_count":ITEM_COUNT, + * "item": [ + * { + * "media_id":MEDIA_ID, + * "content": { + * "news_item": [ + * { + * "title":TITLE, + * "author":AUTHOR, + * "digest":DIGEST, + * "content":CONTENT, + * "content_source_url":CONTENT_SOURCE_URL, + * "thumb_media_id":THUMB_MEDIA_ID, + * "show_cover_pic": 1, + * "need_open_comment": 0, + * "only_fans_can_comment": 0, + * "url":URL + * }, + * //多图文消息会在此处有多篇文章 + * ] + * }, + * "update_time": UPDATE_TIME + * }, + * //可能有多个图文消息item结构 + * ] + * } + * ``` + */ +exports.getDrafts = async function (offset, count, no_content) { + const { accessToken } = await this.ensureAccessToken(); + const url = this.prefix + 'draft/batchget?access_token=' + accessToken; + const data = { + offset: offset, + count: count, + no_content: no_content, }; return this.request(url, postJSON(data)); }; diff --git a/lib/api_media.js b/lib/api_media.js index 2232692..0db1113 100644 --- a/lib/api_media.js +++ b/lib/api_media.js @@ -90,6 +90,21 @@ exports.getMedia = async function (mediaId) { }; return this.request(url, opts); }; + +/** + * 获取临时素材访问链接 + * Examples: + * ``` + * api.getMediaUrl('media_id'); + * ``` + * - `url`, 临时素材访问链接 + * @param {String} mediaId 媒体文件的ID + */ +exports.getMediaUrl = async function (mediaId) { + const { accessToken } = await this.ensureAccessToken(); + return this.prefix + 'media/get?access_token=' + accessToken + '&media_id=' + mediaId; +}; + /** * 上传图文消息内的图片获取URL * 详情请见: diff --git a/lib/api_message.js b/lib/api_message.js index 1f2d7bd..2d3eb96 100644 --- a/lib/api_message.js +++ b/lib/api_message.js @@ -13,7 +13,7 @@ const { postJSON } = require('./util'); * @param {String} openid 用户的openid * @param {String} text 发送的消息内容 */ -exports.sendText = async function (openid, text) { +exports.sendText = async function (openid, text, customservice) { const { accessToken } = await this.ensureAccessToken(); // { // "touser":"OPENID", @@ -30,6 +30,9 @@ exports.sendText = async function (openid, text) { 'content': text } }; + if(customservice) { + data.customservice = customservice + } return this.request(url, postJSON(data)); }; diff --git a/lib/api_subscribe_message.js b/lib/api_subscribe_message.js index 45266be..b599d80 100644 --- a/lib/api_subscribe_message.js +++ b/lib/api_subscribe_message.js @@ -1,6 +1,6 @@ 'use strict'; -const { postJSON, getData } = require('./util'); +const { postJSON } = require('./util'); /** @@ -9,28 +9,30 @@ const { postJSON, getData } = require('./util'); * ``` * var templateId: '模板id'; * var page: ''; - * var data = { + * var data = { character_string1: { value: '223' }, thing5: { value: '测试商品' }, date3: { value: '2020年1月1日' }, number6: { value: '2342375986' }, character_string9: { value: 'sf686539' }, }, - * api.sendSubscribeMessage('openid', templateId, page, data); + * api.sendSubscribeMessage('openid', templateId, page, data, miniprogramState); * ``` * @param {String} openid 用户的openid * @param {String} templateId 模板ID * @param {String} page 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 * @param {Object} data 渲染模板的数据 + * @param {String} miniprogramState 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 */ -exports.sendSubscribeMessage = async function (openid, templateId, page, data, ) { +exports.sendSubscribeMessage = async function (openid, templateId, page, data,miniprogramState ) { const { accessToken } = await this.ensureAccessToken(); var apiUrl = this.prefix + 'message/subscribe/send?access_token=' + accessToken; var template = { touser: openid, template_id: templateId, page: page, - data: data + data: data, + miniprogram_state: miniprogramState, }; return this.request(apiUrl, postJSON(template)); }; diff --git a/lib/api_url.js b/lib/api_url.js index c30b977..96b2d6a 100644 --- a/lib/api_url.js +++ b/lib/api_url.js @@ -18,7 +18,51 @@ exports.shorturl = async function (longUrl) { var url = this.prefix + 'shorturl?access_token=' + accessToken; var data = { 'action': 'long2short', - 'long_url': longUrl + 'long_url': longUrl, }; return this.request(url, postJSON(data)); }; + +/** + * 获取小程序 URL Link,适用于短信、邮件、网页、微信内等拉起小程序的业务场景。目前仅针对国内非个人主体的小程序开放 + * https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html + * { + * "path": "/pages/publishHomework/publishHomework", + * "query": "", + * "expire_type":1, + * "expire_interval":1, + * "env_version": "release", + * "cloud_base": + * { + * "env": "xxx", + * "domain": "xxx.xx", + * "path": "/jump-wxa.html", + * "query": "a=1&b=2" + * } + * } + * + * { + * "errcode": 0, + * "errmsg": "ok", + * "url_link": "URL Link" + * } + * + * Examples: + * ``` + * api.getUrlLink({}); + * ``` + */ +exports.getUrlLink = async function ({ path, query, envVersion, expireType, expireTime, expireInterval, cloudBase }) { + const { accessToken } = await this.ensureAccessToken(); + const apiUrl = this.wxaPrefix + 'generate_urllink?access_token=' + accessToken; + const data = { + path, + query, + env_version: envVersion, + expire_type: expireType, + expire_time: expireTime, + expire_interval: expireInterval, + cloud_base: cloudBase, + }; + return this.request(apiUrl, postJSON(data)); +}; \ No newline at end of file diff --git a/lib/api_user.js b/lib/api_user.js index 1febf0e..a59a492 100644 --- a/lib/api_user.js +++ b/lib/api_user.js @@ -381,7 +381,97 @@ exports.getIdList = async function (openId) { // https://api.weixin.qq.com/cgi-bin/tags/getidlist?access_token=ACCESS_TOKEN var url = this.prefix + 'tags/getidlist?access_token=' + accessToken; var data = { - openid: openId + openid: openId, }; return this.request(url, postJSON(data)); }; + + +/** + * 获取黑名单列表 + * 详细细节 https://developers.weixin.qq.com/doc/offiaccount/User_Management/Manage_blacklist.html + * Examples: + * ``` + * api.getBlackList(beginOpenId); + * ``` + + * Result: + * ``` + * { + * "total":23000, + * "count":10000, + * "data":{" + * openid":[ + * "OPENID1", + * "OPENID2", + * ..., + * "OPENID10000" + * ] + * }, + * "next_openid":"OPENID10000" + * } + * ``` + */ +exports.getBlackList = async function (beginOpenId) { + const { accessToken } = await this.ensureAccessToken(); + // https://api.weixin.qq.com/cgi-bin/tags/members/getblacklist?access_token=ACCESS_TOKEN + var url = this.prefix + 'tags/members/getblacklist?access_token=' + accessToken; + var data = { + begin_openid: beginOpenId, + }; + return this.request(url, postJSON(data)); +}; + +/** + * 拉黑用户 + * 详细细节 https://developers.weixin.qq.com/doc/offiaccount/User_Management/Manage_blacklist.html + * Examples: + * ``` + * api.batchBlack(openIds); + * ``` + + * Result: + * ``` + * { + * "errcode": 0, + * "errmsg": "ok" + * } + * ``` + */ +exports.batchBlack = async function (openIds) { + const { accessToken } = await this.ensureAccessToken(); + // https://api.weixin.qq.com/cgi-bin/tags/members/batchblacklist?access_token=ACCESS_TOKEN + var url = this.prefix + 'tags/members/batchblacklist?access_token=' + accessToken; + var data = { + openid_list: openIds, + }; + return this.request(url, postJSON(data)); +}; + + +/** + * 取消拉黑用户 + * 详细细节 https://developers.weixin.qq.com/doc/offiaccount/User_Management/Manage_blacklist.html + * Examples: + * ``` + * api.batchUnBlack(openIds); + * ``` + + * Result: + * ``` + * { + * "errcode": 0, + * "errmsg": "ok" + * } + * ``` + */ +exports.batchUnBlack = async function (openIds) { + const { accessToken } = await this.ensureAccessToken(); + // https://api.weixin.qq.com/cgi-bin/tags/members/batchunblacklist?access_token=ACCESS_TOKEN + var url = this.prefix + 'tags/members/batchunblacklist?access_token=' + accessToken; + var data = { + openid_list: openIds, + }; + return this.request(url, postJSON(data)); +}; + diff --git a/lib/api_wxacode.js b/lib/api_wxacode.js index d6321b0..7c4d21d 100644 --- a/lib/api_wxacode.js +++ b/lib/api_wxacode.js @@ -62,7 +62,7 @@ exports.getWXACode = async function (path, width = 430, auto_color = false, line * api.getWXACodeUnlimit(scene, page); * ``` * @param {String} scene 最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) - * @param {String} page 必须是已经发布的小程序存在的页面(否则报错),例如 pages/index/index, 根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 + * @param {String} page 例如 pages/index/index, 根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面 * @param {String} width 二维码的宽度,单位 px。最小 280px,最大 1280px * @param {String} auto_color 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 * @param {Object} line_color auto_color 为 false 时生效,使用 rgb 设置颜色 例如 {"r":"xxx","g":"xxx","b":"xxx"} 十进制表示 @@ -75,9 +75,10 @@ exports.getWXACodeUnlimit = async function (scene, page, width = 430, auto_color scene, page, width, + check_path: false, auto_color, line_color, - is_hyaline + is_hyaline, }; return this.request(apiUrl, postJSON(data)); }; diff --git a/package.json b/package.json index 12d800f..8ba0a51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "co-wechat-api", - "version": "3.11.0", + "version": "3.11.1", "description": "微信公共平台Node库API,ES6版本", "main": "index.js", "scripts": { @@ -18,7 +18,8 @@ "dependencies": { "formstream": ">=0.0.8", "httpx": "^2.1.1", - "json-bigint": "^0.3.0" + "json-bigint": "^0.3.0", + "node-fetch": "2.6.1" }, "devDependencies": { "coveralls": "*",