├── .editorconfig ├── .gitignore ├── .huskyrc ├── .npmignore ├── .prettierrc ├── README.md ├── commitlint.config.js ├── demo ├── README.md ├── koa2 │ ├── app.js │ └── package.json └── simple │ ├── index.js │ └── package.json ├── package.json ├── src ├── decorators │ ├── index.ts │ └── login.ts ├── index.ts └── util │ ├── cache.ts │ ├── config.ts │ ├── log.ts │ └── request.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | lib 3 | run.js 4 | qrcode-login.png 5 | qrcode-safe.png 6 | verifycode.png 7 | package-lock.json 8 | cache 9 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | src 3 | run.js 4 | qrcode-login.png 5 | qrcode-safe.png 6 | verifycode.png 7 | tsconfig.json 8 | demo/ 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 200, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechat-mp-hack 2 | 3 | 无需微信认证即可实现微信公众号自动群发图文消息。 4 | 5 | ## 使用 6 | 7 | ```shell 8 | npm i wechat-mp-hack --save 9 | ``` 10 | 11 | ```javascript 12 | const Wechat = require('wechat-mp-hack'); 13 | const API = new Wechat('公众号账号', '公众号密码'); 14 | ``` 15 | 16 | > `1.1.0`版本后不再需要把调用方法包裹在 `login`回调后执行,调用下列核心方式时会自动处理登录。 17 | 18 | ### events 19 | 20 | #### scan.login 21 | 22 | 登录认证二维码 23 | 24 | ```javascript 25 | API.once('scan.login', (filepath) => { 26 | // 登录二维码图片地址 27 | console.log(filepath); 28 | }); 29 | ``` 30 | 31 | #### scan.send 32 | 33 | 开启群发认证保护后调用群发接口需要微信扫描二维码 34 | 35 | ```javascript 36 | API.on('scan.send', (filepath) => { 37 | // 群发认证二维码地址 38 | console.log(filepath); 39 | }); 40 | ``` 41 | 42 | #### vcode 43 | 44 | 登录验证码 45 | 46 | ```javascript 47 | API.once('vcode', (filepath) => { 48 | // 验证码图片地址 49 | console.log(filepath); 50 | }); 51 | ``` 52 | 53 | ### methods 54 | 55 | #### login 56 | 57 | 登录接口 58 | 59 | ```javascript 60 | /** 61 | * @desc 登录公众号 62 | * @param {string} [imgcode] - [可选]验证码 63 | * @return {Promise} data 64 | */ 65 | API.login().then(data => { 66 | console.log(data); 67 | }).catch(console.error); 68 | ``` 69 | 70 | #### loginchk 71 | 72 | 检测是否已经登录 73 | 74 | ```javascript 75 | try { 76 | let islogin = await API.loginchk(); 77 | console.log('已登录'); 78 | } catch(e) { 79 | console.log('未登录'); 80 | } 81 | ``` 82 | 83 | #### appmsg 84 | 85 | 获取图文/视频素材列表 86 | 87 | ```javascript 88 | /** 89 | * 获取图文/视频素材列表 90 | * @param {number} [type] - 消息类型:图文消息-10 视频消息-15 默认-10 91 | * @param {number} [begin] - 从第几条开始 默认-0 92 | * @param {number} [count] - 返回条数 默认-10 93 | * @return {Promise} - 素材列表 94 | * @return {number} [].app_id - 素材id appMsgId 95 | * @return {string} [].author - 作者 96 | * @return {string} [].create_time - 创建时间,单位秒 97 | * @return {number} [].data_seq 98 | * @return {string} [].digest - 素材描述信息 99 | * @return {number} [].file_id 100 | * @return {string} [].img_url - 图片地址 101 | * @return {number} [].is_illegal 102 | * @return {number} [].is_sync_top_stories 103 | * @return {array} [].multi_item - 素材资源列表(一个素材下面有多个文章) 104 | * @return {string} [].multi_item[].author - 文章作者 105 | * @return {string} [].multi_item[].author_appid 106 | * @return {number} [].multi_item[].can_reward - 文章是否可打赏,0否 107 | * @return {string} [].multi_item[].cdn_url - 图片/视频地址 108 | * @return {string} [].multi_item[].cdn_url_back 109 | * @return {number} [].multi_item[].cover - 封面图片地址 110 | * @return {string} [].multi_item[].digest - 文章描述 111 | * @return {number} [].multi_item[].file_id 112 | * @return {string} [].multi_item[].free_content 113 | * @return {number} [].multi_item[].is_new_video 114 | * @return {number} [].multi_item[].need_open_comment 115 | * @return {boolean} [].multi_item[].only_fans_can_comment 116 | * @return {string} [].multi_item[].ori_white_list 117 | * @return {number} [].multi_item[].review_status 118 | * @return {number} [].multi_item[].reward_money 119 | * @return {string} [].multi_item[].reward_wording 120 | * @return {number} [].multi_item[].seq 121 | * @return {number} [].multi_item[].show_cover_pic 122 | * @return {number} [].multi_item[].smart_product 123 | * @return {string} [].multi_item[].source_url - 原文地址 124 | * @return {string} [].multi_item[].title - 文章标题 125 | * @return {array} [].multi_item[].tags - 文章标签 126 | */ 127 | API.appmsg().then((items) => { 128 | console.log(items); 129 | }).catch(console.error); 130 | ``` 131 | 132 | #### filepage 133 | 134 | 获取图片/语音素材列表 135 | 136 | ```javascript 137 | /** 138 | * 获取图片/语音素材列表 139 | * @param {number} [type] - 素材类型:图片素材-2 语音素材-3 默认-2 140 | * @param {number} [begin] - 从第几条开始 默认-0 141 | * @param {number} [count] - 返回条数 默认-10 142 | * @param {number} [group_id] - 图片素材专用,分组id 全部图片-0 未分组-1 文章配图-3 或者其它你自己新建的分组id 143 | * @return {Promise} - 素材列表 144 | * @return {string} [].cdn_url - 资源地址 145 | * @return {number} [].file_id 146 | * @return {number} [].group_id - 分组id 147 | * @return {string} [].img_format - 图片类型:png... 148 | * @return {string} [].name - 资源名称,如:1488631877698.png 149 | * @return {number} [].seq 150 | * @return {string} [].size - 资源大小,如:749.4 K 151 | * @return {number} [].type 152 | * @return {number} [].update_time - 单位:秒 153 | * @return {string} [].video_cdn_id 154 | * @return {string} [].video_thumb_cdn_url 155 | */ 156 | API.filepage().then((files) => { 157 | console.log(files); 158 | }).catch(console.error); 159 | ``` 160 | 161 | #### operate_appmsg 162 | 163 | 创建/更新图文素材 164 | 165 | ```javascript 166 | /** 167 | * 创建图文素材 168 | * @param {array} news - 消息列表 169 | * @param {string} news[].title - 文章标题 170 | * @param {string} news[].thumb - 文章缩略图 171 | * @param {string} news[].description - 描述信息 172 | * @param {string} news[].html - 文章内容 173 | * @param {string} news[].url - 原文地址 174 | * @param {number} [appMsgId] - 图文素材id,传此字段表示更新图文素材 175 | * @return {Promise} appMsgId 176 | */ 177 | API.operate_appmsg(news).then((appMsgId) => { 178 | console.log(appMsgId); 179 | }).catch(console.error); 180 | ``` 181 | 182 | #### del_appmsg 183 | 184 | 删除图文素材 185 | 186 | ```javascript 187 | /** 188 | * 删除图文素材 189 | * @param {number} [appMsgId] - 图文素材id 190 | */ 191 | API.del_appmsg(appMsgId).then(res => { 192 | console.log(res); 193 | console.log('删除成功'); 194 | }); 195 | ``` 196 | 197 | #### batchUpload 198 | 199 | 批量上传远程图片至公众号 200 | 201 | ```javascript 202 | /** 203 | * 批量上传远程图片至公众号 204 | * @param {array} imgurls - 远程图片地址 205 | */ 206 | API.batchUpload(['http://wesbos.com/wp-content/uploads/2016/09/dead-zone.png']).then(results => { 207 | // results[0].fileid; 208 | // results[0].cdn_url; 209 | }); 210 | ``` 211 | 212 | #### filetransfer 213 | 214 | 上传单个远程图片至公众号 215 | 216 | ```javascript 217 | /** 218 | * 上传单个远程图片至公众号 219 | * @param {string} imgurl - 远程图片地址 220 | */ 221 | API.filetransfer('http://wesbos.com/wp-content/uploads/2016/09/dead-zone.png').then(console.log); 222 | ``` 223 | 224 | #### localUpload 225 | 226 | 上传本地图片至公众号 227 | 228 | ```javascript 229 | /** 230 | * 上传本地图片至公众号 231 | * @param {string} filepath - 本地图片地址 232 | * @return {Promise} res 233 | * @return {number} res.fileid - 资源id 234 | * @return {string} res.cdn_url - 资源链接地址 235 | */ 236 | API.localUpload('qrcode-safe.png').then(result => { 237 | console.log(result); 238 | }); 239 | ``` 240 | 241 | #### uploadimg2cdn 242 | 243 | 上传远程图片上传至cdn 244 | 245 | ```javascript 246 | /** 247 | * 上传远程图片上传至cdn 248 | * @param {string} imgurl - 远程图片地址 249 | * @return {Promise} - 微信cdn资源地址 250 | */ 251 | API.uploadimg2cdn('https://www.baidu.com/img/baidu_resultlogo@2.png') 252 | ``` 253 | 254 | #### preview_post 255 | 256 | 获取图文素材文章临时预览链接 257 | 258 | ```javascript 259 | /** 260 | * 获取图文素材文章临时预览链接 261 | * @param {number} appmsgid - 图文素材id 262 | * @param {number} [itemidx] - 文章在图文素材中的索引,从1开始 默认: 1 263 | * @return {Promise} - 文章临时预览链接 264 | */ 265 | API.preview_post(100000126, 2).then(post_url => { 266 | console.log(post_url); 267 | }).catch(console.error); 268 | ``` 269 | 270 | #### preview_appmsg 271 | 272 | 预览群发消息 273 | 274 | ```javascript 275 | /** 276 | * 预览群发消息 277 | * @param {string} username - 预览人微信号/QQ号/手机号 278 | * @param {number|string} content - 预览内容,图文消息-appmsgid 文字-content 图片/语音/视频-fileid 279 | * @param {number} [type] - 消息类型:图文消息-10 文字-1 图片-2 语音-3 视频-15 默认-10 280 | */ 281 | API.preview_appmsg('Zaker-yhz', 100000126).then(res => { 282 | console.log('预览发送成功'); 283 | }).catch(console.error); 284 | ``` 285 | 286 | #### masssend 287 | 288 | 群发消息 289 | 290 | ```javascript 291 | /** 292 | * 群发消息 293 | * @param {number|string} appmsgid - 消息内容,图文消息-appmsgid 文字-文字内容 图片/语音/视频-fileid 294 | * @param {number} [groupid] - 分组id,默认-1 所有用户 295 | * @param {number} [send_time] - 定时群发,默认-0 不定时群发 定时群发设置定时时间戳(单位秒) 296 | * @param {number} [type] - 消息类型:图文消息-10 文字-1 图片-2 语音-3 视频-15 默认-10 297 | */ 298 | API.masssend(appMsgId).then(() => { 299 | console.log('success'); 300 | }).catch(console.error); 301 | ``` 302 | 303 | #### cancel_time_send 304 | 305 | 取消定时群发 306 | 307 | ```javascript 308 | /** 309 | * 取消定时群发 310 | * @param {number} msgid 群发消息id 311 | */ 312 | API.cancel_time_send(1000000041).then(() => { 313 | console.log('success'); 314 | }).catch(console.error); 315 | ``` 316 | 317 | #### timesend_list 318 | 319 | 定时群发消息列表 320 | 321 | ```javascript 322 | /** 323 | * 定时群发消息列表 324 | * @return {Promise} msgs - 定时群发消息列表 325 | * @return {number} msgs[].type - 消息类型 326 | * @return {number} msgs[].msgid - 消息id 327 | * @return {object} msgs[].sent_info 328 | * @return {number} msgs[].sent_info.time - 群发时间 329 | * @return {boolean} msgs[].sent_info.is_send_all - 是否群发给所有人 330 | * @return {array} msgs[].appmsg_info - 图文消息内容 331 | * @return {object} msgs[].text_info - 文字消息 332 | * @return {string} msgs[].text_info.content - 文字消息内容 333 | */ 334 | API.timesend_list().then(msgs => { 335 | console.log(msgs); 336 | }); 337 | ``` 338 | 339 | #### singlesend 340 | 341 | 发文本消息给某个用户 342 | 343 | ```javascript 344 | /** 345 | * 发文本消息给某个用户 346 | * @param {string} tofakeid - 用户fakeid,可以在公众号后台singlesendpage页面url看到或者消息列表 347 | * @param {string} msg - 消息内容 348 | * @param {string} [replyId] - 回复消息id,可以消息列表看到,可选 349 | */ 350 | API.singlesend('osl8HwPBTCsVbquNsnYbUfOQH8sM', '哈哈哈哈', 425131038).then(res => { 351 | console.log(res); 352 | }).catch(console.error); 353 | ``` 354 | 355 | #### message 356 | 357 | 获取公众号消息列表 358 | 359 | ```javascript 360 | /** 361 | * 获取公众号消息列表 362 | * @param {number} count - 消息条数 363 | * @param {number|string} [day] - 今天:0 昨天:1 前天:2 更早:3 最近5天:7 已收藏消息:star,默认:0 364 | * @return {array} msgs 365 | * @return {string} msgs[].content - 消息内容 366 | * @return {string} msgs[].date_time - 消息时间 367 | * @return {string} msgs[].fakeid - 用户fakeid 368 | * @return {number} msgs[].func_flag 369 | * @return {number} msgs[].has_reply 370 | * @return {number} msgs[].id - replyId 371 | * @return {number} msgs[].is_vip_msg 372 | * @return {number} msgs[].msg_status 373 | * @return {array} msgs[].multi_item 374 | * @return {string} msgs[].nick_name 375 | * @return {string} msgs[].refuse_reason 376 | * @return {string} msgs[].source 377 | * @return {string} msgs[].to_uin 378 | * @return {number} msgs[].type 379 | * @return {string} msgs[].wx_headimg_url - 用户头像地址 380 | */ 381 | API.message(1).then(msgs => { 382 | console.log(msgs); 383 | }).catch(console.error); 384 | ``` 385 | 386 | #### user_list 387 | 388 | 获取公众号关注用户列表 389 | 390 | ```javascript 391 | /** 392 | * @desc 获取关注用户列表 393 | * @return {array} userlist 394 | * @return {string} userlist[].user_openid 395 | * @return {string} userlist[].user_name - 用户昵称 396 | * @return {string} userlist[].user_remark - 用户备注名称 397 | * @return {array} userlist[].user_group_id - 分组 398 | * @return {number} userlist[].user_create_time - 关注时间,单位:秒 399 | * @return {string} userlist[].user_head_img - 用户头像地址 400 | */ 401 | API.user_list().then(res => { 402 | console.log(res); 403 | }); 404 | ``` 405 | 406 | #### user_info 407 | 408 | 获取某用户信息 409 | 410 | ```javascript 411 | /** 412 | * 获取用户信息 413 | * @param {string} user_openid 414 | * @return {object} user 415 | * @return {string} user.user_openid 416 | * @return {string} user.user_name - 用户昵称 417 | * @return {string} user.user_remark - 用户备注名称 418 | * @return {array} user.user_group_id - 分组 419 | * @return {number} user.user_create_time - 关注时间,单位:秒 420 | * @return {string} user.user_head_img - 用户头像地址 421 | */ 422 | API.user_info('oslHwqwYnw20jnqMca18KET91pa0').then(res => { 423 | console.log(res); 424 | }); 425 | ``` 426 | 427 | #### user_info_detail 428 | 429 | 获取某用户详细信息 430 | 431 | ```javascript 432 | /** 433 | * 获取用户详细信息 434 | * @param {string} user_openid 435 | * @return {object} user 436 | * @return {string} user.user_openid 437 | * @return {string} user.user_city - 用户城市 438 | * @return {string} user.user_country - 用户国家 439 | * @return {string} user.user_province - 省份 440 | * @return {string} user.user_signature - 签名 441 | * @return {number} user.user_comment_cnt - 留言量 442 | * @return {number} user.user_selected_comment_cnt - 精选留言量 443 | * @return {number} user.user_msg_cnt - 消息量 444 | * @return {number} user.user_gender - 性别 0:未知 1:男 2:女 445 | * @return {string} user.user_name - 用户昵称 446 | * @return {string} user.user_remark - 用户备注名称 447 | * @return {array} user.user_group_id - 分组 448 | * @return {number} user.user_create_time - 关注时间,单位:秒 449 | * @return {string} user.user_head_img - 用户头像地址 450 | * @return {number} user.user_in_blacklist - 是否在黑名单 451 | */ 452 | API.user_info_detail('oslHwqwYnw20jnqMca18KET91pa0').then(res => { 453 | console.log(res); 454 | }); 455 | ``` 456 | 457 | #### qrdecode 458 | 459 | 二维码解析 460 | 461 | ```javascript 462 | /** 463 | * 二维码解析 464 | * @param {string} url - 远程图片地址/本地图片路径 465 | * @return {Promise} 466 | */ 467 | API.qrdecode('qrcode-login.png').then((result) => { 468 | console.log(result.text); 469 | }).catch(console.error); 470 | ``` -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # wechat-mp-hack demo 2 | 3 | - simple: 简单的程序demo 4 | - koa2: 通过微信公众号控制群发 5 | -------------------------------------------------------------------------------- /demo/koa2/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const Wechat = require('co-wechat'); 3 | const WechatHack = require('wechat-mp-hack'); 4 | 5 | const Config = { 6 | token: 'THE TOKEN', 7 | appid: 'THE APPID', 8 | encodingAESKey: 'THE ENCODING AES KEY' 9 | }; 10 | const API = new WechatHack('公众号账号', '公众号密码'); 11 | const handle = async (content, message, needSend = false) => { 12 | let res = ''; 13 | switch(content) { 14 | case '上传': 15 | // 上传素材 16 | let appMsgId = await API.operate_appmsg([{ 17 | title: '这是一个测试图文素材', 18 | thumb: 'https://www.baidu.com/img/superlogo_c4d7df0a003d3db9b65e9ef0fe6da1ec.png', 19 | description: '这里是图文素材的描述', 20 | html: '

这是文章的html内容

', 21 | url: 'https://www.noonme.com' // 文章链接地址 22 | }]); 23 | let temp_url = await API.preview_post(appMsgId); 24 | res = `素材创建成功,第一篇文章查看地址:${temp_url}`; 25 | break; 26 | case '列表': 27 | // 素材列表 28 | let lists = await API.appmsg(10, 0, 5); 29 | res = lists.map(item => { 30 | return { 31 | title: item.title, 32 | description: item.digest, 33 | picurl: item.img_url, 34 | url: '' 35 | }; 36 | }); 37 | break; 38 | case '群发': 39 | // 群发最新的图文素材 40 | let lists = await API.appmsg(10, 0, 1); 41 | let send = await API.masssend(lists[0].app_id); 42 | res = '群发成功'; 43 | break; 44 | } 45 | if (needSend) { 46 | await API.singlesend(message.FromUserName, res); 47 | } 48 | return res; 49 | }; 50 | 51 | const app = new Koa(); 52 | 53 | app.use(Wechat(Config).middleware(async (message, ctx) => { 54 | // 消息内容 55 | const content = (message.Content || '').trim(); 56 | // 获取素材列表 57 | if (['上传', '列表', '群发'].includes(content)) { 58 | try { 59 | const islogin = await API.loginchk(); 60 | const res = await handle(content, message); 61 | return res; 62 | } catch (error) { 63 | // 发送登录认证地址 64 | const qrfilepath = await API.startlogin(); 65 | const result = await API.qrdecode(qrfilepath); 66 | 67 | // 开始检测登录状态 68 | API.loginstep().then(() => { 69 | // 登录成功 70 | handle(content, message, true); 71 | }); 72 | return result.text; 73 | } 74 | } 75 | })); 76 | 77 | app.listen(3000, () => { 78 | console.log('服务启动成功'); 79 | }); -------------------------------------------------------------------------------- /demo/koa2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "wechat-mp-hack demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "keywords": [], 10 | "author": "bukas", 11 | "license": "ISC", 12 | "dependencies": { 13 | "co-wechat": "^2.3.0", 14 | "koa": "^2.5.0", 15 | "wechat-mp-hack": "^1.1.1" 16 | }, 17 | "engines": { 18 | "node": "^8.x" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /demo/simple/index.js: -------------------------------------------------------------------------------- 1 | const Wechat = require('wechat-mp-hack'); 2 | 3 | (async () => { 4 | const API = new Wechat('公众号账号', '公众号密码'); 5 | 6 | API.once('scan.login', (filepath) => { 7 | // 登录二维码图片地址 8 | console.log(filepath); 9 | console.log('请用手机微信扫描二维码确认登录'); 10 | }); 11 | API.once('scan.send', (filepath) => { 12 | // 群发二维码图片地址 13 | console.log(filepath); 14 | console.log('请用手机微信扫描二维码确认群发'); 15 | }); 16 | 17 | // 上传素材 18 | const appMsgId = await API.operate_appmsg([{ 19 | title: '这是一个测试图文素材', 20 | thumb: 'https://www.baidu.com/img/superlogo_c4d7df0a003d3db9b65e9ef0fe6da1ec.png', 21 | description: '这里是图文素材的描述', 22 | html: '

这是文章的html内容

', 23 | url: 'https://www.noonme.com' // 文章原文链接地址 24 | }, { 25 | title: '这是第二篇文章', 26 | thumb: 'https://www.baidu.com/img/superlogo_c4d7df0a003d3db9b65e9ef0fe6da1ec.png', 27 | description: '这里是文章的描述', 28 | html: '

这是文章的html内容

' 29 | }]); 30 | 31 | // 预览一下这个群发图文素材 32 | const p = await API.preview_appmsg('Zaker-yhz', appMsgId, 10); 33 | 34 | // 群发 35 | // const send = await API.masssend(appMsgId); 36 | })(); -------------------------------------------------------------------------------- /demo/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "1.0.0", 4 | "description": "wechat-mp-hack demo", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "bukas", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wechat-mp-hack": "^1.1.1" 13 | }, 14 | "engines": { 15 | "node": "^8.x" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-mp-hack", 3 | "version": "1.1.10", 4 | "description": "微信公众号接口,无需微信认证实现自动创建素材、自动群发图文消息", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib", 9 | "demo" 10 | ], 11 | "scripts": { 12 | "start": "npm run prepublishOnly && npm run dev", 13 | "dev": "node ./run.js", 14 | "prepublishOnly": "rm -rf lib && tsc" 15 | }, 16 | "keywords": [ 17 | "wechat", 18 | "weixin", 19 | "微信公众号", 20 | "微信", 21 | "公众号" 22 | ], 23 | "author": "bukas", 24 | "license": "ISC", 25 | "dependencies": { 26 | "request": "^2.88.2", 27 | "tough-cookie-filestore": "0.0.1" 28 | }, 29 | "devDependencies": { 30 | "@commitlint/cli": "^8.3.5", 31 | "@commitlint/config-conventional": "^8.3.4", 32 | "@types/request": "^2.48.4", 33 | "husky": "^4.2.3", 34 | "typescript": "^3.8.2" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/huanz/wechat-mp-hack.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/huanz/wechat-mp-hack/issues" 42 | }, 43 | "homepage": "https://github.com/huanz/wechat-mp-hack#readme", 44 | "engines": { 45 | "node": "^10.x", 46 | "npm": "^6.x" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login'; 2 | -------------------------------------------------------------------------------- /src/decorators/login.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc login decorator 3 | */ 4 | export function login(target, key, descriptor) { 5 | const func = descriptor.value; 6 | descriptor.value = function() { 7 | let args = arguments; 8 | return this.login().then(() => { 9 | return func.apply(this, args); 10 | }); 11 | }; 12 | return descriptor; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { createWriteStream, createReadStream } from 'fs'; 2 | import { join } from 'path'; 3 | import { EventEmitter } from 'events'; 4 | import { createHash } from 'crypto'; 5 | import request from 'request'; 6 | import WechatRequest from './util/request'; 7 | import Config from './util/config'; 8 | import Log from './util/log'; 9 | import { login } from './decorators/index'; 10 | import Cache from './util/cache'; 11 | 12 | class Wechat extends EventEmitter { 13 | username: string; 14 | pwd: string; 15 | islogin: boolean; 16 | cache: any; 17 | data: any; 18 | 19 | constructor(username, pwd) { 20 | super(); 21 | 22 | this.username = username; 23 | this.pwd = createHash('md5') 24 | .update(pwd.substr(0, 16)) 25 | .digest('hex'); 26 | this.islogin = false; 27 | this.cache = new Cache('wechat'); 28 | this.data = this.cache._data; 29 | } 30 | startlogin(imgcode = '') { 31 | return WechatRequest({ 32 | url: `${Config.api.bizlogin}?action=startlogin`, 33 | form: { 34 | username: this.username, 35 | pwd: this.pwd, 36 | imgcode: imgcode, 37 | f: 'json', 38 | }, 39 | }).then((body: any) => { 40 | if (body.base_resp.ret === 0) { 41 | return this.login_qrcode(); 42 | } else { 43 | // 200023 您输入的帐号或者密码不正确,请重新输入。 44 | // 200008 验证码 45 | if (body.base_resp.ret === 200008) { 46 | this.login_vcode(); 47 | } 48 | throw body; 49 | } 50 | }); 51 | } 52 | login_vcode() { 53 | return new Promise((resolve, reject) => { 54 | let filename = 'verifycode.png'; 55 | let writeStream = createWriteStream(filename); 56 | WechatRequest.get(`${Config.api.verifycode}?username=${this.username}&r=${Date.now()}`) 57 | .pipe(writeStream) 58 | .on('error', reject); 59 | writeStream.on('finish', () => { 60 | this.emit('vcode', filename); 61 | resolve(filename); 62 | }); 63 | }); 64 | } 65 | login_qrcode() { 66 | return new Promise((resolve, reject) => { 67 | let filename = 'qrcode-login.png'; 68 | let writeStream = createWriteStream(filename); 69 | WechatRequest.get(`${Config.api.loginqrcode}?action=getqrcode¶m=4300`) 70 | .pipe(writeStream) 71 | .on('error', reject); 72 | writeStream.on('finish', () => { 73 | this.emit('scan.login', filename); 74 | Log.info('请扫描二维码确认登录!'); 75 | resolve(filename); 76 | }); 77 | }); 78 | } 79 | checkLogin() { 80 | const chklogin = (resolve, reject) => { 81 | WechatRequest.getJSON(`${Config.api.loginqrcode}?action=ask&random=${Math.random()}`) 82 | .then((body: any) => { 83 | if (body.status === 1) { 84 | resolve(body); 85 | } else { 86 | setTimeout(() => { 87 | chklogin(resolve, reject); 88 | }, 3000); 89 | } 90 | }) 91 | .catch(reject); 92 | }; 93 | return new Promise(chklogin); 94 | } 95 | doLogin() { 96 | let loginAction = (resolve, reject) => { 97 | WechatRequest({ 98 | url: `${Config.api.bizlogin}?action=login`, 99 | form: { 100 | f: 'json', 101 | ajax: 1, 102 | random: Math.random(), 103 | }, 104 | }).then((body: any) => { 105 | let token = null; 106 | if (body.base_resp.ret === 0 && (token = body.redirect_url.match(/token=(\d+)/))) { 107 | this.data.token = token[1]; 108 | Log.info('登录成功,token=' + this.data.token); 109 | resolve(token[1]); 110 | } else if (body.base_resp.ret === -1) { 111 | loginAction(resolve, reject); 112 | } else { 113 | reject(body); 114 | } 115 | }); 116 | }; 117 | return new Promise(loginAction); 118 | } 119 | _wechatData() { 120 | return new Promise((resolve, reject) => { 121 | WechatRequest.get(`${Config.api.masssendpage}?t=mass/send&token=${this.data.token}`, (e, r, body) => { 122 | if (e) { 123 | reject(e); 124 | } else { 125 | const ticketMatch = body.match(/ticket:"([\s\S]*?)"/); 126 | const userNameMatch = body.match(/user_name:"([\s\S]*?)"/); 127 | const massProtectMatch = body.match(/"protect_status":(\d+)/); 128 | const operationMatch = body.match(/operation_seq:\s*"(\d+)"/); 129 | if (ticketMatch && userNameMatch) { 130 | this.data.ticket = ticketMatch[1]; 131 | this.data.user_name = userNameMatch[1]; 132 | if (operationMatch) { 133 | this.data.operation_seq = operationMatch[1]; 134 | } 135 | if (massProtectMatch && (2 & massProtectMatch[1]) === 2) { 136 | // 群发保护 137 | this.data.mass_protect = 1; 138 | } 139 | this.islogin = true; 140 | resolve(this.data); 141 | this.cache._data = this.data; 142 | this.cache._save(); 143 | } else { 144 | reject('解析wxdata失败'); 145 | } 146 | } 147 | }); 148 | }); 149 | } 150 | loginstep() { 151 | return this.checkLogin().then(() => this.doLogin().then(() => this._wechatData())); 152 | } 153 | loginchk() { 154 | return new Promise((resolve, reject) => { 155 | if (this.islogin) { 156 | resolve(this.data); 157 | } else if (this.data.token) { 158 | const req: any = WechatRequest.get(Config.baseurl, (error, response, body) => { 159 | if (error) { 160 | reject(error); 161 | } else { 162 | const redirects = req._redirect.redirects; 163 | if (redirects && redirects.length) { 164 | const redirectUri = redirects[redirects.length - 1].redirectUri; 165 | if (/token=(\d+)/.test(redirectUri)) { 166 | this.islogin = true; 167 | resolve(this.data); 168 | } else { 169 | reject(); 170 | } 171 | } else { 172 | reject(); 173 | } 174 | } 175 | }); 176 | } else { 177 | reject(); 178 | } 179 | }); 180 | } 181 | /** 182 | * @desc 登录公众号 183 | * @param {string} imgcode - [可选]验证码 184 | * @return {Promise} data 185 | */ 186 | login(imgcode) { 187 | return new Promise((resolve, reject) => { 188 | this.loginchk() 189 | .then(resolve) 190 | .catch(() => { 191 | this.startlogin(imgcode) 192 | .then(() => { 193 | this.loginstep() 194 | .then(resolve) 195 | .catch(reject); 196 | }) 197 | .catch(reject); 198 | }); 199 | }); 200 | } 201 | /** 202 | * 获取图文/视频素材列表 203 | * @param {number} type - 素材类型:图文素材-10 视频素材-15 默认-10 204 | * @param {number} begin - 从第几条开始 默认-0 205 | * @param {number} count - 返回条数 默认-10 206 | * @return {Promise} - 素材列表 207 | * @return {number} [].app_id - 素材id appMsgId 208 | * @return {string} [].author - 作者 209 | * @return {string} [].title - 标题 210 | * @return {string} [].digest - 素材描述信息 211 | * @return {string} [].img_url - 图片地址 212 | * @return {number} [].file_id 213 | * @return {number} [].is_illegal 214 | * @return {number} [].is_sync_top_stories 215 | * @return {number} [].data_seq 216 | * @return {number} [].seq 217 | * @return {number} [].show_cover_pic 218 | * @return {string} [].create_time - 创建时间,单位秒 219 | * @return {string} [].update_time 220 | * @return {array} [].multi_item - 素材资源列表(一个素材下面有多个文章) 221 | * @return {string} [].multi_item[].author - 文章作者 222 | * @return {string} [].multi_item[].author_appid 223 | * @return {number} [].multi_item[].can_reward - 文章是否可打赏,0否 224 | * @return {string} [].multi_item[].cdn_url - 图片/视频地址 225 | * @return {string} [].multi_item[].cdn_url_back 226 | * @return {number} [].multi_item[].cover - 封面图片地址 227 | * @return {string} [].multi_item[].digest - 文章描述 228 | * @return {number} [].multi_item[].file_id 229 | * @return {string} [].multi_item[].free_content 230 | * @return {number} [].multi_item[].is_new_video 231 | * @return {number} [].multi_item[].need_open_comment 232 | * @return {boolean} [].multi_item[].only_fans_can_comment 233 | * @return {string} [].multi_item[].ori_white_list 234 | * @return {number} [].multi_item[].review_status 235 | * @return {number} [].multi_item[].reward_money 236 | * @return {string} [].multi_item[].reward_wording 237 | * @return {number} [].multi_item[].seq 238 | * @return {number} [].multi_item[].show_cover_pic 239 | * @return {number} [].multi_item[].smart_product 240 | * @return {string} [].multi_item[].source_url - 原文地址 241 | * @return {string} [].multi_item[].title - 文章标题 242 | * @return {array} [].multi_item[].tags - 文章标签 243 | */ 244 | @login 245 | appmsg(type = 10, begin = 0, count = 10) { 246 | return WechatRequest.getJSON(`${Config.api.appmsg}?begin=${begin}&count=${count}&type=${type}&token=${this.data.token}&action=${type === 15 ? 'list_video' : 'list_card'}`).then((body: any) => { 247 | if (body.base_resp.ret === 0) { 248 | return body.app_msg_info.item; 249 | } else { 250 | throw body.base_resp.err_msg; 251 | } 252 | }); 253 | } 254 | /** 255 | * 获取图片/语音素材列表 256 | * @param {number} type - 素材类型:图片素材-2 语音素材-3 默认-2 257 | * @param {number} begin - 从第几条开始 默认-0 258 | * @param {number} count - 返回条数 默认-10 259 | * @param {number} group_id - 图片素材专用,分组id 全部图片-0 未分组-1 文章配图-3 或者其它你自己新建的分组id 260 | * @return {Promise} - 素材列表 261 | * @return {string} [].cdn_url - 资源地址 262 | * @return {number} [].file_id 263 | * @return {number} [].group_id - 分组id 264 | * @return {string} [].img_format - 图片类型:png... 265 | * @return {string} [].name - 资源名称,如:1488631877698.png 266 | * @return {number} [].seq 267 | * @return {string} [].size - 资源大小,如:749.4 K 268 | * @return {number} [].type 269 | * @return {number} [].update_time - 单位:秒 270 | * @return {string} [].video_cdn_id 271 | * @return {string} [].video_thumb_cdn_url 272 | */ 273 | @login 274 | filepage(type = 2, begin = 0, count = 10, group_id = 0) { 275 | return WechatRequest.getJSON(`${Config.api.filepage}?begin=${begin}&count=${count}&type=${type}&token=${this.data.token}&group_id=${group_id}`).then((body: any) => { 276 | if (body.base_resp.ret === 0) { 277 | return body.page_info.file_item; 278 | } else { 279 | throw body.base_resp.err_msg; 280 | } 281 | }); 282 | } 283 | /** 284 | * 创建图文素材 285 | * @param {array} news - 消息列表 286 | * @param {string} news[].title - 文章标题 287 | * @param {string} news[].thumb - 文章缩略图 288 | * @param {string} news[].description - 描述信息 289 | * @param {string} news[].html - 文章内容 290 | * @param {string} news[].url - 原文地址 291 | * @param {number} [appMsgId] - 图文素材id,传此字段表示更新图文素材 292 | * @return {Promise} appMsgId 293 | */ 294 | @login 295 | operate_appmsg(news, appMsgId) { 296 | return new Promise((resolve, reject) => { 297 | let uploadImgs = []; 298 | let postNews = news.filter(item => { 299 | let hasThumb = !!item.thumb; 300 | if (hasThumb) { 301 | uploadImgs.push(item.thumb); 302 | } 303 | return hasThumb; 304 | }); 305 | if (uploadImgs.length) { 306 | this.parseNewsList(postNews) 307 | .then(newsObj => { 308 | this._operate_appmsg(newsObj, postNews.length, appMsgId) 309 | .then(resolve) 310 | .catch(reject); 311 | }) 312 | .catch(reject); 313 | } else { 314 | reject('至少有一篇新闻具有图片'); 315 | } 316 | }); 317 | } 318 | /** 319 | * 删除图文素材 320 | * @param {number} [appMsgId] - 图文素材id 321 | */ 322 | @login 323 | del_appmsg(appMsgId: number) { 324 | return WechatRequest({ 325 | url: `${Config.api.operate_appmsg}?t=ajax-response&sub=del`, 326 | headers: { 327 | Referer: `${Config.api.appmsg}?begin=0&count=10&t=media/appmsg_list&type=10&action=list&lang=zh_CN&token=${this.data.token}`, 328 | }, 329 | form: { 330 | AppMsgId: appMsgId, 331 | token: this.data.token, 332 | f: 'json', 333 | ajax: 1, 334 | }, 335 | }).then((body: any) => { 336 | if (body.base_resp.ret === 0) { 337 | return body; 338 | } else { 339 | let msg = Log.msg(body.base_resp.ret); 340 | Log.error(msg); 341 | body.msg = msg; 342 | throw body; 343 | } 344 | }); 345 | } 346 | parseNewsList(newsList) { 347 | return Promise.all(newsList.map((item, index) => this.parseNews(item, index))).then(paramArr => { 348 | return Object.assign.apply(Object, paramArr); 349 | }); 350 | } 351 | parseNews(news, index) { 352 | let pattern = /]+src=['"]([^'"]+)['"]+/g; 353 | let promiseArr = []; 354 | let imgs = []; 355 | let temp = null; 356 | while ((temp = pattern.exec(news.html)) !== null && imgs.indexOf(temp[1]) === -1 && !this.isLocalDomain(temp[1])) { 357 | promiseArr.push(this.uploadimg2cdn(temp[1])); 358 | imgs.push(temp[1]); 359 | } 360 | /** 361 | * 上传缩略图 362 | */ 363 | promiseArr.push(this.uploadimg2cdn(news.thumb)); 364 | 365 | return Promise.all(promiseArr).then(urls => { 366 | imgs.forEach((imgurl, i) => { 367 | news.html = news.html.replace(new RegExp(imgurl, 'gi'), urls[i]); 368 | }); 369 | news.cdn_url = urls[urls.length - 1]; 370 | return this._newsToMpParam(news, index); 371 | }); 372 | } 373 | isLocalDomain(url) { 374 | let localReg = [ 375 | /^http(s)?:\/\/mmbiz\.qpic\.cn([\/?].*)*$/i, 376 | /^http(s)?:\/\/mmbiz\.qlogo\.cn([\/?].*)*$/i, 377 | /^http(s)?:\/\/m\.qpic\.cn([\/?].*)*$/i, 378 | /^http(s)?:\/\/mmsns\.qpic\.cn([\/?].*)*$/i, 379 | /^http(s)?:\/\/mp\.weixin\.qq\.com([\/?].*)*$/i, 380 | /^http(s)?:\/\/(a|b)(\d)+\.photo\.store\.qq\.com([\/?].*)*$/i, 381 | ]; 382 | return localReg.some(pattern => pattern.test(url)); 383 | } 384 | _operate_appmsg(wechatNews, count, appMsgId) { 385 | const params: any = { 386 | token: this.data.token, 387 | f: 'json', 388 | ajax: 1, 389 | random: Math.random(), 390 | }; 391 | if (appMsgId) { 392 | params.AppMsgId = appMsgId; 393 | } 394 | if (count) { 395 | params.count = count; 396 | Object.assign(params, wechatNews); 397 | } else { 398 | params.count = wechatNews.length; 399 | Object.assign(params, this._transformToMpParam(wechatNews)); 400 | } 401 | return WechatRequest({ 402 | url: `${Config.api.operate_appmsg}?t=ajax-response&sub=${appMsgId ? 'update' : 'create'}&type=10&token=${this.data.token}`, 403 | headers: { 404 | Referer: `${Config.api.appmsg}?t=media/appmsg_edit&action=edit&type=10&isMul=1&isNew=1&token=${this.data.token}`, 405 | }, 406 | form: params, 407 | }).then((body: any) => { 408 | if (body.base_resp.ret === 0) { 409 | return body.appMsgId; 410 | } else { 411 | let msg = Log.msg(body.base_resp.ret); 412 | Log.error(msg); 413 | body.msg = msg; 414 | throw body; 415 | } 416 | }); 417 | } 418 | /** 419 | * 数组变成微信参数 420 | * title html 421 | */ 422 | _transformToMpParam(arr) { 423 | let obj = {}; 424 | arr.forEach((item, index) => { 425 | Object.assign(obj, this._newsToMpParam(item, index)); 426 | }); 427 | return obj; 428 | } 429 | _newsToMpParam(item, index) { 430 | const obj = {}; 431 | obj[`title${index}`] = item.title; 432 | obj[`content${index}`] = item.html; 433 | obj[`digest${index}`] = item.description; 434 | obj[`fileid${index}`] = item.fileid || ''; // 图片微信id 435 | obj[`cdn_url${index}`] = item.cdn_url; 436 | obj[`digest${index}`] = item.description; 437 | obj[`sourceurl${index}`] = item.url; 438 | obj[`show_cover_pic${index}`] = 0; 439 | obj[`need_open_comment${index}`] = 1; 440 | obj[`music_id${index}`] = ''; 441 | obj[`video_id${index}`] = ''; 442 | obj[`shortvideofileid${index}`] = ''; 443 | obj[`copyright_type${index}`] = ''; 444 | obj[`only_fans_can_comment${index}`] = ''; 445 | obj[`fee${index}`] = ''; 446 | obj[`voteid${index}`] = ''; 447 | obj[`voteismlt${index}`] = ''; 448 | obj[`ad_id${index}`] = ''; 449 | return obj; 450 | } 451 | /** 452 | * 批量上传远程图片至公众号 453 | * @param {array} imgurls - 远程图片地址 454 | * @return {Promise} 455 | */ 456 | @login 457 | batchUpload(imgurls) { 458 | return Promise.all(imgurls.map(imgurl => this.filetransfer(imgurl))); 459 | } 460 | /** 461 | * 上传单个远程图片至公众号 462 | * @param {string} imgurl - 远程图片地址 463 | * @return {Promise} 464 | */ 465 | @login 466 | filetransfer(imgurl) { 467 | return new Promise((resolve, reject) => { 468 | const filename = join(Config.upload, Date.now() + '.png'); 469 | const writeStream = createWriteStream(filename); 470 | request(imgurl) 471 | .pipe(writeStream) 472 | .on('error', reject); 473 | writeStream.on('finish', () => 474 | this.localUpload(filename) 475 | .then(resolve) 476 | .catch(reject) 477 | ); 478 | }); 479 | } 480 | /** 481 | * 上传本地图片至公众号 482 | * @param {string} filepath - 本地图片地址 483 | * @return {Promise} res 484 | * @return {number} res.fileid - 资源id 485 | * @return {string} res.cdn_url - 资源链接地址 486 | */ 487 | @login 488 | localUpload(filepath) { 489 | return WechatRequest({ 490 | url: `${Config.api.filetransfer}?action=upload_material&f=json&scene=1&writetype=doublewrite&groupid=1&ticket_id=${this.data.user_name}&ticket=${this.data.ticket}&svr_time=${Math.floor( 491 | Date.now() / 1000 492 | )}&seq=1&token=${this.data.token}`, 493 | headers: { 494 | Referer: `${Config.api.filepage}?type=2&begin=0&count=12&t=media/img_list&token=${this.data.token}`, 495 | }, 496 | formData: { 497 | file: createReadStream(filepath), 498 | }, 499 | }).then((body: any) => { 500 | if (body.base_resp.ret === 0) { 501 | return { 502 | fileid: body.content, 503 | cdn_url: body.cdn_url, 504 | }; 505 | } else { 506 | throw body; 507 | } 508 | }); 509 | } 510 | /** 511 | * 上传远程图片上传至cdn 512 | * @param {string} imgurl - 远程图片地址 513 | * @return {Promise} - 微信cdn资源地址 514 | */ 515 | @login 516 | uploadimg2cdn(imgurl) { 517 | return WechatRequest({ 518 | url: `${Config.api.uploadimg2cdn}?token=${this.data.token}`, 519 | form: { 520 | imgurl: imgurl, 521 | t: 'ajax-editor-upload-img', 522 | }, 523 | }).then((body: any) => { 524 | if (body.errcode === 0) { 525 | return body.url; 526 | } else { 527 | throw body; 528 | } 529 | }); 530 | } 531 | /** 532 | * 群发消息 533 | * @param {number|string} appmsgid - 消息内容,图文消息-appmsgid 文字-文字内容 图片/语音/视频-fileid 534 | * @param {number} groupid - 分组id,默认-1 所有用户 535 | * @param {number} send_time - 定时群发,默认-0 不定时群发 定时群发设置定时时间戳(单位秒) 536 | * @param {number} type - 消息类型:图文消息-10 文字-1 图片-2 语音-3 视频-15 默认-10 537 | */ 538 | @login 539 | masssend(appmsgid, groupid = -1, send_time = 0, type = 10) { 540 | const params: any = { 541 | type: type, 542 | groupid: groupid, 543 | send_time: send_time, 544 | }; 545 | if (type === 10) { 546 | params.appmsgid = appmsgid; 547 | } else if (type === 1) { 548 | params.content = appmsgid; 549 | } else { 550 | params.fileid = appmsgid; 551 | } 552 | if (this.data.mass_protect) { 553 | return new Promise((resolve, reject) => { 554 | this.getticket() 555 | .then(body => { 556 | this.getuuid(body.ticket) 557 | .then(uuid => { 558 | this.checkuuid(uuid, body.ticket, body.operation_seq) 559 | .then((res: any) => { 560 | params.operation_seq = body.operation_seq; 561 | params.code = res.code; 562 | this.safesend(params) 563 | .then(resolve) 564 | .catch(reject); 565 | }) 566 | .catch(reject); 567 | }) 568 | .catch(reject); 569 | }) 570 | .catch(reject); 571 | }); 572 | } else { 573 | params.operation_seq = this.data.operation_seq; 574 | return this.safesend(params); 575 | } 576 | } 577 | /** 578 | * 获取图文素材文章临时预览链接 579 | * @param {number} appmsgid - 图文素材id 580 | * @param {number} itemidx - 文章在图文素材中的索引,从1开始 默认: 1 581 | * @return {Promise} - 文章临时预览链接 582 | */ 583 | @login 584 | preview_post(appmsgid, itemidx = 1) { 585 | return WechatRequest.getJSON(`${Config.api.appmsg}?action=get_temp_url&appmsgid=${appmsgid}&itemidx=${itemidx}&token=${this.data.token}`).then((body: any) => { 586 | if (body.base_resp.ret === 0) { 587 | return body.temp_url; 588 | } else { 589 | throw body; 590 | } 591 | }); 592 | } 593 | /** 594 | * 预览群发消息 595 | * @param {string} username - 预览人微信号/QQ号/手机号 596 | * @param {number|string} content - 预览内容,图文消息-appmsgid 文字-文字内容 图片/语音/视频-fileid 597 | * @param {number} type - 消息类型:图文消息-10 文字-1 图片-2 语音-3 视频-15 默认-10 598 | */ 599 | @login 600 | preview_appmsg(username, content, type = 10) { 601 | const params: any = { 602 | token: this.data.token, 603 | f: 'json', 604 | ajax: 1, 605 | random: Math.random(), 606 | type: type, 607 | preusername: username, 608 | is_preview: 1, 609 | }; 610 | if (type === 10) { 611 | params.appmsgid = content; 612 | } else if (type === 1) { 613 | params.content = content; 614 | } else { 615 | params.fileid = content; 616 | } 617 | return WechatRequest({ 618 | url: `${Config.api.operate_appmsg}?t=ajax-appmsg-preview&token=${this.data.token}&sub=preview&type=${type}`, 619 | form: params, 620 | }).then((body: any) => { 621 | if (body.base_resp.ret === 0) { 622 | return body; 623 | } else { 624 | let msg = Log.msg(body.base_resp.ret); 625 | body.base_resp.err_msg = msg; 626 | body.msg = msg; 627 | throw body; 628 | } 629 | }); 630 | } 631 | /** 632 | * 获取群发ticket 633 | */ 634 | getticket() { 635 | Log.info('获取群发ticket'); 636 | return WechatRequest({ 637 | url: `${Config.api.safeassistant}?1=1&token=${this.data.token}`, 638 | form: { 639 | token: this.data.token, 640 | f: 'json', 641 | ajax: 1, 642 | random: Math.random(), 643 | action: 'get_ticket', 644 | }, 645 | }).then((body: any) => { 646 | if (body.base_resp.ret === 0) { 647 | Log.info('群发ticket获取成功'); 648 | return { 649 | ticket: body.ticket, 650 | operation_seq: body.operation_seq, 651 | }; 652 | } else { 653 | Log.info('群发ticket获取失败'); 654 | throw body; 655 | } 656 | }); 657 | } 658 | getuuid(ticket) { 659 | return WechatRequest({ 660 | url: `${Config.api.safeqrconnect}?1=1&token=${this.data.token}`, 661 | form: { 662 | token: this.data.token, 663 | f: 'json', 664 | ajax: 1, 665 | random: Math.random(), 666 | state: 0, 667 | login_type: 'safe_center', 668 | type: 'json', 669 | ticket: ticket, 670 | }, 671 | }).then((body: any) => { 672 | if (body.uuid) { 673 | Log.info('成功获取uuid'); 674 | return body.uuid; 675 | } else { 676 | throw body; 677 | } 678 | }); 679 | } 680 | checkuuid(uuid, ticket, operation_seq) { 681 | let douuid = (resolve, reject) => { 682 | WechatRequest({ 683 | url: `${Config.api.safeuuid}?timespam=${Date.now()}&token=${this.data.token}`, 684 | form: { 685 | token: this.data.token, 686 | f: 'json', 687 | ajax: 1, 688 | random: Math.random(), 689 | uuid: uuid, 690 | action: 'json', 691 | type: 'json', 692 | }, 693 | }) 694 | .then((body: any) => { 695 | if (body.errcode == 405) { 696 | Log.info('成功扫描群发认证二维码!'); 697 | resolve(body); 698 | } else { 699 | setTimeout(() => { 700 | douuid(resolve, reject); 701 | }, 3000); 702 | } 703 | }) 704 | .catch(reject); 705 | }; 706 | return new Promise((resolve, reject) => { 707 | let filename = 'qrcode-safe.png'; 708 | let writeStream = createWriteStream(filename); 709 | WechatRequest.get(`${Config.api.safeqrcode}?action=check&type=msgs&ticket=${ticket}&uuid=${uuid}&msgid=${operation_seq}`) 710 | .pipe(writeStream) 711 | .on('error', reject); 712 | writeStream.on('finish', () => { 713 | this.emit('scan.send', filename); 714 | Log.info('请扫描群发认证二维码!'); 715 | douuid(resolve, reject); 716 | }); 717 | }); 718 | } 719 | copyright(appmsgid, type = 10) { 720 | const check = first => { 721 | return WechatRequest({ 722 | url: `${Config.api.masssend}?action=get_appmsg_copyright_stat&token=${this.data.token}`, 723 | form: { 724 | token: this.data.token, 725 | f: 'json', 726 | ajax: 1, 727 | first_check: first, 728 | appmsgid: appmsgid, 729 | type: type, 730 | }, 731 | }); 732 | }; 733 | return check(1).then(() => check(0)); 734 | } 735 | safesend(params) { 736 | return params.appmsgid ? this.copyright(params.appmsgid).then(() => this._real_send(params)) : this._real_send(params); 737 | } 738 | _real_send(params) { 739 | return WechatRequest({ 740 | url: `${Config.api.masssend}?t=ajax-response&token=${this.data.token}${params.send_time ? '&action=time_send' : ''}`, 741 | form: Object.assign( 742 | { 743 | token: this.data.token, 744 | f: 'json', 745 | ajax: 1, 746 | random: Math.random(), 747 | smart_product: 0, 748 | cardlimit: 1, 749 | sex: 0, 750 | synctxweibo: 0, 751 | direct_send: 1, 752 | req_id: this._getid(32), 753 | req_time: Date.now(), 754 | }, 755 | params 756 | ), 757 | }).then((body: any) => { 758 | if (body.base_resp.ret === 0) { 759 | return body; 760 | } else { 761 | Log.error(body); 762 | throw body; 763 | } 764 | }); 765 | } 766 | /** 767 | * 取消定时群发 768 | * @param {number} msgid 群发消息id 769 | */ 770 | @login 771 | cancel_time_send(msgid) { 772 | return WechatRequest({ 773 | url: `${Config.api.masssendpage}?action=cancel_time_send&token=${this.data.token}`, 774 | form: { 775 | id: msgid, 776 | token: this.data.token, 777 | f: 'json', 778 | ajax: 1, 779 | }, 780 | }).then((body: any) => { 781 | if (body.base_resp.ret === 0) { 782 | return body; 783 | } else { 784 | Log.error(body); 785 | throw body; 786 | } 787 | }); 788 | } 789 | /** 790 | * 定时群发消息列表 791 | * @return {Promise} msgs - 定时群发消息列表 792 | * @return {number} msgs[].type - 消息类型 793 | * @return {number} msgs[].msgid - 消息id 794 | * @return {object} msgs[].sent_info 795 | * @return {number} msgs[].sent_info.time - 群发时间 796 | * @return {boolean} msgs[].sent_info.is_send_all - 是否群发给所有人 797 | * @return {array} msgs[].appmsg_info - 图文消息内容 798 | * @return {object} msgs[].text_info - 文字消息 799 | * @return {string} msgs[].text_info.content - 文字消息内容 800 | */ 801 | @login 802 | timesend_list() { 803 | return WechatRequest.getJSON(`${Config.api.home}?t=home/index&token=${this.data.token}`).then((body: any) => { 804 | if (body.base_resp.ret === 0) { 805 | let msgs = JSON.parse(body.timesend_msg); 806 | return msgs.sent_list; 807 | } else { 808 | Log.error(body); 809 | throw body; 810 | } 811 | }); 812 | } 813 | /** 814 | * 发文本消息给某个用户 815 | * @param {string} tofakeid - 用户fakeid,可以在公众号后台singlesendpage页面url看到或者消息列表 816 | * @param {string} msg - 消息内容 817 | * @param {string} replyId - 回复消息id,可以消息列表看到,可选 818 | */ 819 | @login 820 | singlesend(tofakeid, msg, replyId = '') { 821 | return WechatRequest({ 822 | url: `${Config.api.singlesend}?t=ajax-response&f=json&token=${this.data.token}`, 823 | form: { 824 | token: this.data.token, 825 | f: 'json', 826 | ajax: 1, 827 | random: Math.random(), 828 | type: 1, 829 | content: msg, 830 | tofakeid: tofakeid, 831 | quickReplyId: replyId, 832 | imgcode: '', 833 | }, 834 | }).then((body: any) => { 835 | if (body.base_resp.ret === 0) { 836 | return body; 837 | } else { 838 | Log.error(body); 839 | throw body; 840 | } 841 | }); 842 | } 843 | /** 844 | * 获取公众号消息列表 845 | * @param {number} count - 消息条数 846 | * @param {number|string} day - 今天:0 昨天:1 前天:2 更早:3 最近5天:7 已收藏消息:star,默认:0 847 | * @return {array} msgs 848 | * @return {string} msgs[].content - 消息内容 849 | * @return {string} msgs[].date_time - 消息时间 850 | * @return {string} msgs[].fakeid - 用户fakeid 851 | * @return {number} msgs[].func_flag 852 | * @return {number} msgs[].has_reply 853 | * @return {number} msgs[].id - replyId 854 | * @return {number} msgs[].is_vip_msg 855 | * @return {number} msgs[].msg_status 856 | * @return {array} msgs[].multi_item 857 | * @return {string} msgs[].nick_name 858 | * @return {string} msgs[].refuse_reason 859 | * @return {string} msgs[].source 860 | * @return {string} msgs[].to_uin 861 | * @return {number} msgs[].type 862 | * @return {string} msgs[].wx_headimg_url - 用户头像地址 863 | */ 864 | @login 865 | message(count: number, day: number | string = 0) { 866 | let url = `${Config.api.message}?t=message/list&f=json&filtertype=0&filterivrmsg=0&filterspammsg=0&count=${count}&token=${this.data.token}`; 867 | url += day === 'star' ? '&action=star' : `&day=${day}`; 868 | return WechatRequest.getJSON(url).then((body: any) => { 869 | if (body.base_resp.ret === 0) { 870 | return JSON.parse(body.msg_items); 871 | } else { 872 | throw body.base_resp.err_msg; 873 | } 874 | }); 875 | } 876 | /** 877 | * @desc 获取关注用户列表 878 | * @return {array} userlist 879 | * @return {string} userlist[].user_openid 880 | * @return {string} userlist[].user_name - 用户昵称 881 | * @return {string} userlist[].user_remark - 用户备注名称 882 | * @return {array} userlist[].user_group_id - 分组 883 | * @return {number} userlist[].user_create_time - 关注时间,单位:秒 884 | * @return {string} userlist[].user_head_img - 用户头像地址 885 | */ 886 | @login 887 | user_list() { 888 | return WechatRequest.getJSON(`${Config.api.userlist}?action=get_all_data&lang=zh_CN&f=json&token=${this.data.token}`).then((body: any) => { 889 | if (body.base_resp.ret === 0) { 890 | let userlist = body.user_list.user_info_list; 891 | // 换成大logo 892 | userlist.map(user => { 893 | if (user.user_head_img && user.user_head_img.endsWith('/64')) { 894 | user.user_head_img = user.user_head_img.replace(/64$/, '0'); 895 | } 896 | return user; 897 | }); 898 | this.cache.set('userlist', userlist); 899 | return userlist; 900 | } else { 901 | throw body.base_resp.err_msg; 902 | } 903 | }); 904 | } 905 | /** 906 | * 获取用户信息 907 | * @param {string} user_openid 908 | * @return {object} user 909 | * @return {string} user.user_openid 910 | * @return {string} user.user_name - 用户昵称 911 | * @return {string} user.user_remark - 用户备注名称 912 | * @return {array} user.user_group_id - 分组 913 | * @return {number} user.user_create_time - 关注时间,单位:秒 914 | * @return {string} user.user_head_img - 用户头像地址 915 | */ 916 | user_info(user_openid) { 917 | const userlist = this.cache.get('userlist'); 918 | if (userlist && userlist.length) { 919 | let user = userlist.find(user => user.user_openid === user_openid); 920 | return user ? Promise.resolve(user) : this._remote_find(user_openid); 921 | } else { 922 | return this._remote_find(user_openid); 923 | } 924 | } 925 | _remote_find(user_openid) { 926 | return this.user_list().then(users => { 927 | return users.find(user => user.user_openid === user_openid); 928 | }); 929 | } 930 | /** 931 | * 获取用户详细信息 932 | * @param {string} user_openid 933 | * @return {object} user 934 | * @return {string} user.user_openid 935 | * @return {string} user.user_city - 用户城市 936 | * @return {string} user.user_country - 用户国家 937 | * @return {string} user.user_province - 省份 938 | * @return {string} user.user_signature - 签名 939 | * @return {number} user.user_comment_cnt - 留言量 940 | * @return {number} user.user_selected_comment_cnt - 精选留言量 941 | * @return {number} user.user_msg_cnt - 消息量 942 | * @return {number} user.user_gender - 性别 0:未知 1:男 2:女 943 | * @return {string} user.user_name - 用户昵称 944 | * @return {string} user.user_remark - 用户备注名称 945 | * @return {array} user.user_group_id - 分组 946 | * @return {number} user.user_create_time - 关注时间,单位:秒 947 | * @return {string} user.user_head_img - 用户头像地址 948 | * @return {number} user.user_in_blacklist - 是否在黑名单 949 | */ 950 | @login 951 | user_info_detail(user_openid) { 952 | return WechatRequest({ 953 | url: `${Config.api.userlist}?action=get_fans_info`, 954 | form: { 955 | token: this.data.token, 956 | user_openid: user_openid, 957 | }, 958 | }).then((body: any) => { 959 | if (body.base_resp.ret === 0) { 960 | return body.user_list.user_info_list[0]; 961 | } else { 962 | throw body.base_resp.err_msg; 963 | } 964 | }); 965 | } 966 | _getid(len) { 967 | let id = ''; 968 | let str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 969 | for (let i = 0; i < len; i++) { 970 | id += str.charAt(Math.floor(Math.random() * str.length)); 971 | } 972 | return id; 973 | } 974 | /** 975 | * 二维码解析 976 | * @param {string} url - 远程图片地址/本地图片路径 977 | * @return {Promise} 978 | */ 979 | qrdecode(url) { 980 | return new Promise((resolve, reject) => { 981 | const formData: any = {}; 982 | if (/^https?:\/\//.test(url)) { 983 | formData.url = url; 984 | } else { 985 | try { 986 | formData.qrcode = createReadStream(url); 987 | } catch (error) { 988 | reject(error); 989 | } 990 | } 991 | request( 992 | { 993 | method: 'POST', 994 | url: 'http://tool.oschina.net/action/qrcode/decode', 995 | headers: { 996 | Host: 'tool.oschina.net', 997 | Referer: 'http://tool.oschina.net/qr?type=2', 998 | Origin: 'http://tool.oschina.net', 999 | 'User-Agent': Config.userAgent, 1000 | 'Upgrade-Insecure-Requests': 1, 1001 | }, 1002 | json: true, 1003 | formData: formData, 1004 | }, 1005 | (e, r, body) => { 1006 | e ? reject(e) : resolve(body[0]); 1007 | } 1008 | ); 1009 | }); 1010 | } 1011 | } 1012 | 1013 | process.on('unhandledRejection', error => { 1014 | Log.error(error); 1015 | }); 1016 | 1017 | export = Wechat; 1018 | -------------------------------------------------------------------------------- /src/util/cache.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, readFileSync, writeFileSync, writeFile } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export default class Cache { 5 | cacheDir: string; 6 | cacheFile: string; 7 | _data: any; 8 | 9 | constructor(cacheName: string) { 10 | this.cacheDir = join(__dirname, '..', 'cache'); 11 | if (!existsSync(this.cacheDir)) { 12 | mkdirSync(this.cacheDir); 13 | } 14 | this.cacheFile = join(this.cacheDir, `${cacheName}.json`); 15 | try { 16 | let data = readFileSync(this.cacheFile, 'utf8'); 17 | this._data = JSON.parse(data); 18 | } catch (error) { 19 | writeFileSync(this.cacheFile, '{}'); 20 | this._data = {}; 21 | } 22 | } 23 | get(key: string) { 24 | return this._data[key]; 25 | } 26 | set(key: string, value: any) { 27 | this._data[key] = value; 28 | this._save(); 29 | } 30 | clear() { 31 | this._data = {}; 32 | this._save(); 33 | } 34 | _save() { 35 | writeFile(this.cacheFile, JSON.stringify(this._data), 'utf8', () => {}); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/util/config.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdir } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | const HOST = 'mp.weixin.qq.com'; 5 | const BASEURL = `https://${HOST}`; 6 | 7 | const API = { 8 | home: `${BASEURL}/cgi-bin/home`, 9 | masssendpage: `${BASEURL}/cgi-bin/masssendpage`, 10 | bizlogin: `${BASEURL}/cgi-bin/bizlogin`, 11 | loginqrcode: `${BASEURL}/cgi-bin/loginqrcode`, 12 | operate_appmsg: `${BASEURL}/cgi-bin/operate_appmsg`, 13 | appmsg: `${BASEURL}/cgi-bin/appmsg`, 14 | filetransfer: `${BASEURL}/cgi-bin/filetransfer`, 15 | filepage: `${BASEURL}/cgi-bin/filepage`, 16 | masssend: `${BASEURL}/cgi-bin/masssend`, 17 | safeassistant: `${BASEURL}/misc/safeassistant`, 18 | safeqrconnect: `${BASEURL}/safe/safeqrconnect`, 19 | safeqrcode: `${BASEURL}/safe/safeqrcode`, 20 | safeuuid: `${BASEURL}/safe/safeuuid`, 21 | singlesend: `${BASEURL}/cgi-bin/singlesend`, 22 | message: `${BASEURL}/cgi-bin/message`, 23 | uploadimg2cdn: `${BASEURL}/cgi-bin/uploadimg2cdn`, 24 | verifycode: `${BASEURL}/cgi-bin/verifycode`, 25 | userlist: `${BASEURL}/cgi-bin/user_tag`, 26 | }; 27 | 28 | const Config = { 29 | host: HOST, 30 | baseurl: BASEURL, 31 | api: API, 32 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', 33 | upload: join(process.cwd(), 'upload'), 34 | }; 35 | 36 | if (!existsSync(Config.upload)) { 37 | mkdir(Config.upload, () => {}); 38 | } 39 | 40 | export default Config; 41 | -------------------------------------------------------------------------------- /src/util/log.ts: -------------------------------------------------------------------------------- 1 | const ERROR_MAP = { 2 | '-6': '请输入验证码', 3 | '-8': '请输入验证码', 4 | '62752': '可能含有具备安全风险的链接,请检查', 5 | '64505': '发送预览失败,请稍后再试', 6 | '64504': '保存图文消息发送错误,请稍后再试', 7 | '64518': '正文只能包含一个投票', 8 | '10704': '该素材已被删除', 9 | '10705': '该素材已被删除', 10 | '10701': '用户已被加入黑名单,无法向其发送消息', 11 | '10703': '对方关闭了接收消息', 12 | '10700': '1.接收预览消息的微信尚未关注公众号,请先扫码关注
2.如果已经关注公众号,请查看微信的隐私设置(在手机微信的“我->设置->隐私->添加我的方式”中),并开启“可通过以下方式找到我”的“手机号”、“微信号”、“QQ号”,否则可能接收不到预览消息', 13 | '64503': '1.接收预览消息的微信尚未关注公众号,请先扫码关注
2.如果已经关注公众号,请查看微信的隐私设置(在手机微信的“我->设置->隐私->添加我的方式”中),并开启“可通过以下方式找到我”的“手机号”、“微信号”、“QQ号”,否则可能接收不到预览消息', 14 | '64502': '你输入的微信号不存在,请重新输入', 15 | '64501': '你输入的帐号不存在,请重新输入', 16 | '412': '图文中含非法外链', 17 | '64515': '当前素材非最新内容,请重新打开并编辑', 18 | '320001': '该素材已被删除,无法保存', 19 | '64702': '标题超出64字长度限制', 20 | '64703': '摘要超出120字长度限制', 21 | '64704': '推荐语超出140字长度限制', 22 | '200041': '此素材有文章存在违规,无法编辑', 23 | '64506': '保存失败,链接不合法', 24 | '64507': '内容不能包含链接,请调整', 25 | '64510': '内容不能包含语音,请调整', 26 | '64511': '内容不能包多个语音,请调整', 27 | '64512': '文章中语音错误,请使用语音添加按钮重新添加。', 28 | '64508': '查看原文链接可能具备安全风险,请检查', 29 | '64550': '请勿插入不合法的图文消息链接', 30 | '64558': '请勿插入图文消息临时链接,链接会在短期失效', 31 | '64559': '请勿插入未群发的图文消息链接', 32 | '-99': '内容超出字数,请调整', 33 | '64705': '内容超出字数,请调整', 34 | '-1': '系统错误,请注意备份内容后重试', 35 | '-2': '参数错误,请注意备份内容后重试', 36 | '200002': '参数错误,请注意备份内容后重试', 37 | '64509': '正文中不能包含超过3个视频,请重新编辑正文后再保存。', 38 | '-5': '服务错误,请注意备份内容后重试。', 39 | '-206': '目前,服务负荷过大,请稍后重试。', 40 | '10801': '标题不能有违反公众平台协议、相关法律法规和政策的内容,请重新编辑。', 41 | '10802': '作者不能有违反公众平台协议、相关法律法规和政策的内容,请重新编辑。', 42 | '10803': '敏感链接,请重新添加。', 43 | '10804': '摘要不能有违反公众平台协议、相关法律法规和政策的内容,请重新编辑。', 44 | '10806': '正文不能有违反公众平台协议、相关法律法规和政策的内容,请重新编辑。', 45 | '10807': '内容不能违反公众平台协议、相关法律法规和政策,请重新编辑。', 46 | '-2e4': '登录态超时,请重新登录。', 47 | '64513': '封面必须存在正文中,请检查封面', 48 | '64551': '请检查图文消息中的微视链接后重试。', 49 | '64552': '请检查阅读原文中的链接后重试。', 50 | '64553': '请不要在图文消息中插入超过5张卡券。请删减卡券后重试。', 51 | '64554': '在当前情况下不允许在图文消息中插入卡券,请删除卡券后重试。', 52 | '64555': '请检查图文消息卡片跳转的链接后重试。', 53 | '64556': '卡券不属于该公众号,请删除后重试', 54 | '64557': '卡券无效,请删除后重试。', 55 | '13002': '该广告卡片已过期,删除后才可保存成功', 56 | '13003': '已有文章插入过该广告卡片,一个广告卡片仅可插入一篇文章', 57 | '13004': '该广告卡片与图文消息位置不一致', 58 | '15801': '你所编辑的内容可能含有违反微信公众平台平台协议、相关法律法规和政策的内容', 59 | '15802': '你所编辑的内容可能含有违反微信公众平台平台协议、相关法律法规和政策的内容', 60 | '15803': '你所编辑的内容可能含有违反微信公众平台平台协议、相关法律法规和政策的内容', 61 | '15804': '你所编辑的内容可能含有违反微信公众平台平台协议、相关法律法规和政策的内容', 62 | '15805': '你所编辑的内容可能含有违反微信公众平台平台协议、相关法律法规和政策的内容', 63 | '15806': '你所编辑的内容可能含有违反微信公众平台平台协议、相关法律法规和政策的内容', 64 | '1530503': '请勿添加其他公众号的主页链接', 65 | '1530504': '请勿添加其他公众号的主页链接', 66 | '1530510': '链接已失效,请在手机端重新复制链接', 67 | '153007': '很抱歉,原创声明不成功|你的文章内容未达到声明原创的要求,满足以下任一条件可发起声明:
1、文章文字大于300字
2、文章文字小于300字,视频均为你已成功声明原创的视频
3、文章文字小于300字,无视频,图片(包括封面图)均为你已成功声明原创的图片', 68 | '153008': '很抱歉,原创声明不成功|你的文章内容少于300字,未达到申请原创内容声明的字数要求。', 69 | '153009': '很抱歉,原创声明不成功|你的文章内容未达到声明原创的要求,满足以下任一条件可发起声明:
1、文章文字大于300字
2、文章文字小于300字,无视频,图片(包括封面图)均为你已成功声明原创的图片', 70 | '153010': '很抱歉,原创声明不成功|你的文章内容未达到声明原创的要求,满足以下任一条件可发起声明:
1、文章文字大于300字
2、文章文字小于300字,视频均为你已成功声明原创的视频', 71 | '1530511': '链接已失效,请在手机端重新复制链接', 72 | '220001': '"素材管理"中的存储数量已达到上限,请删除后再操作。', 73 | '220002': '你的图片库已达到存储上限,请进行清理。', 74 | '153012': '请设置转载类型', 75 | '200042': '图文中包含的小程序卡片不能多于20个', 76 | '200043': '图文中包含没有关联的小程序,请删除后再保存', 77 | '64601': '一篇文章只能插入一个广告卡片', 78 | '64602': '尚未开通文中广告位,但文章中有广告', 79 | '64603': '文中广告前不足300字', 80 | '64604': '文中广告后不足300字', 81 | '64605': '文中不能同时插入文中广告和互选广告', 82 | '65101': '图文模版数量已达到上限,请删除后再操作', 83 | '64560': '请勿插入历史图文消息页链接', 84 | '64561': '请勿插入mp.weixin.qq.com域名下的非图文消息链接', 85 | '64562': '请勿插入非mp.weixin.qq.com域名的链接', 86 | '67016': '视频还在审核中,若审核失败则将无法播放', 87 | '67015': '视频已被下架或删除,无法播放,请重新选择', 88 | '67012': '设置失败,定时时间与已有互选广告订单时间冲突', 89 | '67013': '设置失败,定时时间超过卡券有效期', 90 | '200013': '操作太频繁,请稍后再试', 91 | '67014': '该时刻定时消息过多,请选择其他时刻', 92 | '67011': '设置的定时群发时间错误,请重新选择', 93 | '64004': '今天的群发数量已到,无法群发或剩余定时群发数量不足', 94 | '67008': '消息中可能含有具备安全风险的链接,请检查', 95 | '200008': '请输入验证码', 96 | '14002': '没有“审核通过”的门店。确认有至少一个“审核通过”的门店后可进行卡券投放。', 97 | '200001': '文章包含的语音已被删除,请重新添加。', 98 | '14003': '投放用户缺少测试权限,请先设置白名单', 99 | '67010': '本月发表付费文章已达10篇', 100 | }; 101 | 102 | const msg = code => { 103 | return ERROR_MAP[code] || '系统繁忙,请稍后重试'; 104 | }; 105 | 106 | const userMsg = e => { 107 | if (typeof e === 'object' && typeof e.base_resp === 'object') { 108 | e.base_resp.msg = msg(e.base_resp.ret); 109 | } 110 | return e; 111 | }; 112 | 113 | const Log = { 114 | info(e) { 115 | console.log(`\x1b[34m${JSON.stringify(userMsg(e))}\x1b[0m`); 116 | }, 117 | error(e) { 118 | console.error(`\x1b[31m${JSON.stringify(userMsg(e))}\x1b[0m`); 119 | }, 120 | msg, 121 | }; 122 | 123 | export default Log; 124 | -------------------------------------------------------------------------------- /src/util/request.ts: -------------------------------------------------------------------------------- 1 | import request from 'request'; 2 | import FileCookieStore from 'tough-cookie-filestore'; 3 | import Config from './config'; 4 | import Log from './log'; 5 | import Cache from './cache'; 6 | 7 | const cookieCache = new Cache('cookie'); 8 | const j = request.jar(new FileCookieStore(cookieCache.cacheFile)); 9 | const r = request.defaults({ 10 | method: 'POST', 11 | headers: { 12 | Referer: Config.baseurl, 13 | Host: Config.host, 14 | 'User-Agent': Config.userAgent, 15 | 'X-Requested-With': 'XMLHttpRequest', 16 | }, 17 | json: true, 18 | jar: j, 19 | qs: { 20 | lang: 'zh_CN', 21 | }, 22 | followAllRedirects: true, 23 | followOriginalHttpMethod: true, 24 | }); 25 | 26 | const WechatRequest = options => { 27 | return new Promise((resolve, reject) => { 28 | r(options, (e, r, body) => { 29 | if (e) { 30 | reject(e); 31 | Log.error(e); 32 | } else { 33 | resolve(body); 34 | } 35 | }); 36 | }); 37 | }; 38 | 39 | WechatRequest.get = r.get; 40 | 41 | WechatRequest.getJSON = (url, options?) => { 42 | return WechatRequest({ 43 | method: 'GET', 44 | url: url, 45 | qs: { 46 | f: 'json', 47 | ajax: 1, 48 | }, 49 | ...options, 50 | }); 51 | }; 52 | 53 | WechatRequest.cookies = () => { 54 | let cookies = j.getCookies(Config.baseurl); 55 | let obj = {}; 56 | cookies.forEach(c => { 57 | obj[c.key] = c.value; 58 | }); 59 | return obj; 60 | }; 61 | 62 | export default WechatRequest; 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "noImplicitAny": false, 8 | "removeComments": true, 9 | "noLib": false, 10 | "allowSyntheticDefaultImports": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "esnext", 14 | "sourceMap": false, 15 | "outDir": "lib/", 16 | "baseUrl": "." 17 | }, 18 | "include": ["./src"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------