We Chat API 源码 -立哥开发

//Copyright 2008-2021 Jacky Zong . All rights reserved.
//  江山如此多娇,
//  引无数英雄竞折腰;
//  惜秦皇汉武
//  略输文采
import fs from 'fs';

import AxiosError from 'axios-error';
import FormData from 'form-data';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import invariant from 'ts-invariant';
import warning from 'warning';
import {
  OnRequestFunction,
  camelcaseKeys,
  createRequestInterceptor,
  onRequest,
  snakecaseKeys,
} from 'messaging-api-common';

import * as WechatTypes from './WechatTypes';

function throwErrorIfAny(response: AxiosResponse): AxiosResponse {
  const { errcode, errmsg } = response.data;
  if (!errcode || errcode === 0) return response;
  const msg = `WeChat API - ${errcode} ${errmsg}`;
  throw new AxiosError(msg, {
    response,
    config: response.config,
    request: response.request,
  });
}

export default class WechatClient {
  /**
   * @deprecated Use `new WechatClient(...)` instead.
   */
  static connect(config: WechatTypes.ClientConfig): WechatClient {
    warning(
      false,
      '`WechatClient.connect(...)` is deprecated. Use `new WechatClient(...)` instead.'
    );
    return new WechatClient(config);
  }

  /**
   * The underlying axios instance.
   */
  readonly axios: AxiosInstance;

  /**
   * The current access token used by the client.
   */
  accessToken = '';

  /**
   * The callback to be called when receiving requests.
   */
  private onRequest?: OnRequestFunction;

  /**
   * The app ID used by the client.
   */
  private appId: string;

  /**
   * The app secret used by the client.
   */
  private appSecret: string;

  /**
   * The timestamp of the token expired time.
   */
  private tokenExpiresAt = 0;

  constructor(config: WechatTypes.ClientConfig) {
    invariant(
      typeof config !== 'string',
      `WechatClient: do not allow constructing client with ${config} string. Use object instead.`
    );

    this.appId = config.appId;
    this.appSecret = config.appSecret;
    this.onRequest = config.onRequest || onRequest;
    const { origin } = config;

    this.axios = axios.create({
      baseURL: `${origin || 'https://api.weixin.qq.com'}/cgi-bin/`,
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.axios.interceptors.request.use(
      createRequestInterceptor({
        onRequest: this.onRequest,
      })
    );
  }

  private async refreshToken(): Promise<void> {
    const { accessToken, expiresIn } = await this.getAccessToken();

    this.accessToken = accessToken;
    this.tokenExpiresAt = Date.now() + expiresIn * 1000;
  }

  private async refreshTokenWhenExpired(): Promise<void> {
    if (Date.now() > this.tokenExpiresAt) {
      await this.refreshToken();
    }
  }

  /**
   * 获取 access_token
   *
   * @returns Access token info
   *

   *
   * @example
   *
   * ```js
   * await client.getAccessToken();
   * // {
   * //   accessToken: "ACCESS_TOKEN",
   * //   expiresIn: 7200
   * // }
   * ```
   */
  getAccessToken(): Promise<WechatTypes.AccessToken> {
    return this.axios
      .get<
        | { access_token: string; expires_in: number }
        | WechatTypes.FailedResponseData
      >(
        `/token?grant_type=client_credential&appid=${this.appId}&secret=${this.appSecret}`
      )
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

  /**
   * 临时素材
   *
   * 媒体文件保存时间为 3 天,即 3 天后 media_id 失效。
   *
   * 图片(image)- 2M,支持 PNG,JPEG,JPG,GIF 格式
   * 语音(voice)- 2M,播放长度不超过 60s,支持 AMR,MP3 格式
   * 视频(video)- 10MB,支持 MP4 格式
   * 缩略图(thumb)- 64KB,支持 JPG 格式
   */

  /**
   * 多媒体文件上传接口
   *
   * @param type - Type of the media to upload.
   * @param media - Buffer or stream of the media to upload.
   * @returns Info of the uploaded media.
   *
  
   *
   * @example
   *
   * ```js
   * const fs = require('fs');
   *
   * const buffer = fs.readFileSync('test.jpg');
   *
   * await client.uploadMedia('image', buffer);
  
   */
  async uploadMedia(
    type: WechatTypes.MediaType,
    media: Buffer | fs.ReadStream
  ): Promise<WechatTypes.UploadedMedia> {
    await this.refreshTokenWhenExpired();

    const form = new FormData();

    form.append('media', media);

    return this.axios
      .post<
        | { type: string; media_id: string; created_at: number }
        | WechatTypes.FailedResponseData
      >(`/media/upload?access_token=${this.accessToken}&type=${type}`, form, {
        headers: form.getHeaders(),
      })
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

  /**
   * 下载多媒体文件接口
   *
   * @param mediaId - ID of the media to get.
   * @returns Info of the media.
   *

   *
   * @example
   *
   * ```js
   * await client.getMedia(MEDIA_ID);
   * // {
   * //   videoUrl: "..."
   * // }
   * ```
   */
  async getMedia(mediaId: string): Promise<WechatTypes.Media> {
    await this.refreshTokenWhenExpired();

    return this.axios
      .get<{ video_url: string } | WechatTypes.FailedResponseData>(
        `/media/get?access_token=${this.accessToken}&media_id=${mediaId}`
      )
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

  /**
   * 发送消息-客服消息
   *
   * @internal
   *
   
   */
  async sendRawBody(
    body: {
      touser: string;
    } & WechatTypes.SendMessageOptions &
      (
        | {
            msgtype: 'text';
            text: {
              content: string;
            };
          }
        | {
            msgtype: 'image';
            image: {
              mediaId: string;
            };
          }
        | {
            msgtype: 'voice';
            voice: {
              mediaId: string;
            };
          }
        | {
            msgtype: 'video';
            video: WechatTypes.Video;
          }
        | {
            msgtype: 'music';
            music: WechatTypes.Music;
          }
        | {
            msgtype: 'news';
            news: WechatTypes.News;
          }
        | {
            msgtype: 'mpnews';
            mpnews: {
              mediaId: string;
            };
          }
        | {
            msgtype: 'msgmenu';
            msgmenu: WechatTypes.MsgMenu;
          }
        | {
            msgtype: 'wxcard';
            wxcard: {
              cardId: string;
            };
          }
        | {
            msgtype: 'miniprogrampage';
            miniprogrampage: WechatTypes.MiniProgramPage;
          }
      )
  ): Promise<WechatTypes.SucceededResponseData> {
    await this.refreshTokenWhenExpired();

    return this.axios
      .post<WechatTypes.ResponseData>(
        `/message/custom/send?access_token=${this.accessToken}`,
        snakecaseKeys(body, { deep: true })
      )
      .then(throwErrorIfAny)
      .then(
        (res) =>
          camelcaseKeys(res.data, {
            deep: true,
          }) as any
      );
  }

  /**
   * 发送文本消息
   *
   * @param userId - User ID of the recipient
   * @param text - Text to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   
   *
   * @example
   *
   * ```js
   * await client.sendText(USER_ID, 'Hello!');
   * ```
   */
  sendText(
    userId: string,
    text: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'text',
      text: {
        content: text,
      },
      ...options,
    });
  }

  /**
   * 发送图片消息
   *
   * @param userId - User ID of the recipient
   * @param mediaId - ID of the media to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   
   *
   * @example
   *
   * ```js
   * await client.sendImage(USER_ID, 'MEDIA_ID');
   * ```
   */
  sendImage(
    userId: string,
    mediaId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'image',
      image: {
        mediaId,
      },
      ...options,
    });
  }

  /**
   * 发送语音消息
   *
   * @param userId - User ID of the recipient
   * @param mediaId - ID of the media to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   * @see https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#7
   *
   * @example
   *
   * ```js
   * await client.sendVoice(USER_ID, 'MEDIA_ID');
   * ```
   */
  sendVoice(
    userId: string,
    mediaId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'voice',
      voice: {
        mediaId,
      },
      ...options,
    });
  }

  /**
   * 发送视频消息
   *
   * @param userId - User ID of the recipient
   * @param video - Info of the video to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
 
   *
   * @example
   *
   * ```js
   * await client.sendVideo(USER_ID, {
   *   mediaId: 'MEDIA_ID',
   *   thumbMediaId: 'THUMB_MEDIA_ID',
   *   title: 'VIDEO_TITLE',
   *   description: 'VIDEO_DESCRIPTION',
   * });
   * ```
   */
  sendVideo(
    userId: string,
    video: WechatTypes.Video,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'video',
      video,
      ...options,
    });
  }

  /**
   * 发送音乐消息
   *
   * @param userId - User ID of the recipient
   * @param news - Data of the music to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   
   *
   * @example
   *
   * ```js
   * await client.sendMusic(USER_ID, {
   *   musicurl: 'MUSIC_URL',
   *   hqmusicurl: 'HQ_MUSIC_URL',
   *   thumbMediaId: 'THUMB_MEDIA_ID',
   *   title: 'MUSIC_TITLE',
   *   description: 'MUSIC_DESCRIPTION',
   * });
   * ```
   */
  sendMusic(
    userId: string,
    music: WechatTypes.Music,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'music',
      music,
      ...options,
    });
  }

  /**
   * 发送图文消息(点击跳转到外链)
   *
   * 图文消息条数限制在 8 条以内,注意,如果图文数超过 8,则将会无响应。
   *
   * @param userId - User ID of the recipient
   * @param news - Data of the news to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
  
   *
   * @example
   *
   * ```js
   * await client.sendNews(USER_ID, {
   *   articles: [
   *     {
   *       title: 'Happy Day',
   *       description: 'Is Really A Happy Day',
   *       url: 'URL',
   *       picurl: 'PIC_URL',
   *     },
   *     {
   *       title: 'Happy Day',
   *       description: 'Is Really A Happy Day',
   *       url: 'URL',
   *       picurl: 'PIC_URL',
   *     },
   *   ],
   * });
   * ```
   */
  sendNews(
    userId: string,
    news: WechatTypes.News,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'news',
      news,
      ...options,
    });
  }

  /**
   * 发送图文消息(点击跳转到图文消息页面)
   *
   * 图文消息条数限制在 8 条以内,注意,如果图文数超过 8,则将会无响应。
   *
   * @param userId - User ID of the recipient
   * @param mediaId - ID of the media to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *

   *
   * @example
   *
   * ```js
   * await client.sendMPNews(USER_ID, 'MEDIA_ID');
   * ```
   */
  sendMPNews(
    userId: string,
    mediaId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'mpnews',
      mpnews: {
        mediaId,
      },
      ...options,
    });
  }

  /**
   * 发送菜单消息
   *
   * @param userId - User ID of the recipient
   * @param msgMenu - Data of the msg menu to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   * @see https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#7
   *
   * @example
   *
   * ```js
   * await client.sendMsgMenu(USER_ID, {
   *   headContent: 'HEAD',
   *   list: [
   *     {
   *       id: '101',
   *       content: 'Yes',
   *     },
   *     {
   *       id: '102',
   *       content: 'No',
   *     },
   *   ],
   *   'tailContent': 'Tail',
   * });
   * ```
   */
  sendMsgMenu(
    userId: string,
    msgMenu: WechatTypes.MsgMenu,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'msgmenu',
      msgmenu: msgMenu,
      ...options,
    });
  }

  /**
   * 发送卡券
   *
   * @param userId - User ID of the recipient
   * @param cardId - ID of the card to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   * @see https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#7
   *
   * @example
   *
   * ```js
   * await client.sendWXCard(USER_ID, '123dsdajkasd231jhksad');
   * ```
   */
  sendWXCard(
    userId: string,
    cardId: string,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'wxcard',
      wxcard: {
        cardId,
      },
      ...options,
    });
  }

  /**
   * 发送小程序卡片(要求小程序与公众号已关联)
   *
   * @param userId - User ID of the recipient
   * @param miniProgramPage - Info of the mini program page to be sent.
   * @param options - The other parameters.
   * @returns Error code and error message.
   *
   * @see https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#7
   *
   * @example
   *
   * ```js
   * await client.sendMiniProgramPage(USER_ID, {
   *   title: 'title',
   *   appid: 'appid',
   *   pagepath: 'pagepath',
   *   thumbMediaId: 'thumb_media_id',
   * });
   * ```
   */
  sendMiniProgramPage(
    userId: string,
    miniProgramPage: WechatTypes.MiniProgramPage,
    options?: WechatTypes.SendMessageOptions
  ): Promise<WechatTypes.SucceededResponseData> {
    return this.sendRawBody({
      touser: userId,
      msgtype: 'miniprogrampage',
      miniprogrampage: miniProgramPage,
      ...options,
    });
  }

  // TODO: implement typing

  // TODO: 客服帳號相關
  
  }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值