Spring Bootアプリケーションから企業WeChat(WeChat Work)のグループボットへメッセージを送信する機能は、システム監視、アラート通知、レポート配信など、さまざまな自動化シナリオで役立ちます。本記事では、この通知機能を実装するためのJavaクラス構造とHTTPクライアントの実装について解説します。
メッセージ送信APIインターフェース
まず、企業WeChatボットとの連携に必要な基本的な操作を定義するインターフェースを設計します。これには、各種メッセージの送信とファイルのアップロード機能が含まれます。
import java.io.File;
/**
* 企業WeChatボットとのAPI通信インターフェース
*/
public interface WeChatBotApiClient {
/**
* 指定されたペイロードに基づいてメッセージを送信します。
* @param botKey ボットのユニークな識別子
* @param payload 送信するメッセージのデータ構造
* @return APIからの応答
*/
ApiResponse sendMessage(String botKey, MessagePayload payload);
/**
* JSON形式のメッセージ文字列を直接送信します。
* @param botKey ボットのユニークな識別子
* @param jsonMessage 送信するメッセージのJSON文字列
* @return APIからの応答
*/
ApiResponse sendMessage(String botKey, String jsonMessage);
/**
* ファイルまたは音声をアップロードします。
* ファイルはmultipart/form-data形式でPOSTされます。
*
* すべてのファイルタイプは5バイト以上である必要があります。
* 通常ファイル(FILE): サイズは20MB以下
* 音声ファイル(VOICE): サイズは2MB以下、再生時間は60秒以下、AMR形式のみサポート
*
* @param botKey ボットのユニークな識別子
* @param mediaType ファイルの種類 ("file" または "voice")
* @param file アップロードするファイル
* @return アップロードAPIからの応答
*/
MediaUploadResponse uploadMedia(String botKey, String mediaType, File file);
}
メッセージペイロードの基本構造
企業WeChatボットに送信するメッセージは、特定の形式に従う必要があります。すべてのメッセージペイロードの基底クラスとして、メッセージタイプを保持する抽象クラスを定義します。
import java.util.Objects;
/**
* すべてのメッセージペイロードの基底クラス
*/
public abstract class MessagePayload {
private final String messageType;
protected MessagePayload(String type) {
this.messageType = type;
}
public String getMessageType() {
return messageType;
}
@Override
public String toString() {
return "MessagePayload(messageType=" + this.getMessageType() + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MessagePayload that = (MessagePayload) o;
return Objects.equals(messageType, that.messageType);
}
@Override
public int hashCode() {
return Objects.hash(messageType);
}
}
特定のメッセージタイプ用ペイロード
さまざまなメッセージタイプ(ファイル、画像、Markdown、ニュース、テンプレートカード、テキスト、音声)に対応するペイロードクラスは、それぞれMessagePayloadを継承し、特定のデータ構造をカプセル化します。
ファイルメッセージ
import java.util.Objects;
/**
* ファイルタイプメッセージのデータ構造
*/
public class FileMessage extends MessagePayload {
private final FileContent file;
private FileMessage(FileContent fileData) {
super("file");
this.file = fileData;
}
public FileContent getFile() {
return file;
}
@Override
public String toString() {
return "FileMessage{" +
"file=" + file +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
FileMessage that = (FileMessage) o;
return Objects.equals(file, that.file);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), file);
}
/**
* ファイルメッセージを構築します。
* @param mediaId ファイルのメディアID (アップロードAPIで取得)
*/
public static FileMessage of(String mediaId) {
if (mediaId == null || mediaId.trim().isEmpty()) {
throw new IllegalArgumentException("メディアIDは空であってはなりません");
}
return new FileMessage(new FileContent(mediaId));
}
/**
* ファイルメッセージの具体的な内容
*/
public static class FileContent {
/**
* ファイルのメディアID
*/
private String media_id;
public FileContent(String mediaId) {
this.media_id = mediaId;
}
public String getMedia_id() {
return media_id;
}
public void setMedia_id(String mediaId) {
this.media_id = mediaId;
}
@Override
public String toString() {
return "FileContent{" +
"media_id='" + media_id + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FileContent that = (FileContent) o;
return Objects.equals(media_id, that.media_id);
}
@Override
public int hashCode() {
return Objects.hash(media_id);
}
}
}
画像メッセージ
import java.util.Objects;
/**
* 画像タイプメッセージのデータ構造
*/
public class ImageMessage extends MessagePayload {
private final ImageData image;
private ImageMessage(ImageData imgData) {
super("image");
this.image = imgData;
}
public ImageData getImage() {
return image;
}
@Override
public String toString() {
return "ImageMessage{" +
"image=" + image +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
ImageMessage that = (ImageMessage) o;
return Objects.equals(image, that.image);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), image);
}
/**
* 画像メッセージを構築します。
* @param base64Content 画像コンテンツのBase64エンコード文字列
* @param md5Hash Base64エンコード前の画像コンテンツのMD5ハッシュ値
*/
public static ImageMessage of(String base64Content, String md5Hash) {
return new ImageMessage(new ImageData(base64Content, md5Hash));
}
/**
* 画像メッセージの具体的な内容
*/
public static class ImageData {
/**
* 画像コンテンツのBase64エンコード文字列
*/
private String base64;
/**
* Base64エンコード前の画像コンテンツのMD5ハッシュ値
*/
private String md5;
public ImageData(String base64, String md5) {
this.base64 = base64;
this.md5 = md5;
}
public String getBase64() {
return base64;
}
public void setBase64(String base64) {
this.base64 = base64;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
@Override
public String toString() {
return "ImageData{" +
"base64='" + base64 + '\'' +
", md5='" + md5 + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImageData imageData = (ImageData) o;
return Objects.equals(base64, imageData.base64) &&
Objects.equals(md5, imageData.md5);
}
@Override
public int hashCode() {
return Objects.hash(base64, md5);
}
}
}
Markdownメッセージ
import java.util.Objects;
public class MarkdownMessage extends MessagePayload {
private final MarkdownContent markdown;
private MarkdownMessage(MarkdownContent markdownData) {
super("markdown");
this.markdown = markdownData;
}
public MarkdownContent getMarkdown() {
return markdown;
}
@Override
public String toString() {
return "MarkdownMessage{" +
"markdown=" + markdown +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
MarkdownMessage that = (MarkdownMessage) o;
return Objects.equals(markdown, that.markdown);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), markdown);
}
/**
* Markdownメッセージを構築します。
* @param content Markdownコンテンツ (最大4096バイト、UTF-8エンコード)
*/
public static MarkdownMessage of(String content) {
return new MarkdownMessage(new MarkdownContent(content));
}
/**
* Markdownメッセージの具体的な内容
*/
public static class MarkdownContent {
/**
* Markdown内容。標準Markdown構文と一部HTMLタグをサポート。
* サポートされる色タグ: <font color="warning">、<font color="comment">、<font color="info"> など
*/
private String content;
public MarkdownContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "MarkdownContent{" +
"content='" + content + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MarkdownContent that = (MarkdownContent) o;
return Objects.equals(content, that.content);
}
@Override
public int hashCode() {
return Objects.hash(content);
}
}
}
ニュース記事メッセージ(複数の記事をまとめたもの)
import java.util.List;
import java.util.Objects;
/**
* ニュース記事タイプメッセージのデータ構造
*/
public class NewsArticleMessage extends MessagePayload {
/**
* ニュース記事のリスト (1〜8件までサポート)
*/
private final ArticleList news;
private NewsArticleMessage(ArticleList newsList) {
super("news");
this.news = newsList;
}
public ArticleList getNews() {
return news;
}
@Override
public String toString() {
return "NewsArticleMessage{" +
"news=" + news +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
NewsArticleMessage that = (NewsArticleMessage) o;
return Objects.equals(news, that.news);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), news);
}
/**
* ニュース記事メッセージを構築します。
* @param articles ニュース記事のリスト (1〜8件)
*/
public static NewsArticleMessage of(List<NewsEntry> articles) {
return new NewsArticleMessage(new ArticleList(articles));
}
/**
* ニュース記事メッセージの具体的な内容
*/
public static class ArticleList {
private List<NewsEntry> articles;
public ArticleList(List<NewsEntry> articles) {
this.articles = articles;
}
public List<NewsEntry> getArticles() {
return articles;
}
public void setArticles(List<NewsEntry> articles) {
this.articles = articles;
}
@Override
public String toString() {
return "ArticleList{" +
"articles=" + articles +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArticleList that = (ArticleList) o;
return Objects.equals(articles, that.articles);
}
@Override
public int hashCode() {
return Objects.hash(articles);
}
}
/**
* 個々のニュース記事エントリー
*/
public static class NewsEntry {
/**
* タイトル (最大128バイト)
*/
private String title;
/**
* 説明 (最大512バイト、オプション)
*/
private String description;
/**
* クリック時のリンク先URL
*/
private String url;
/**
* 画像のURL (オプション)
* JPG/PNG形式をサポート。推奨サイズ: 大画像1068 * 455、小画像150 * 150
*/
private String imageUrl;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
@Override
public String toString() {
return "NewsEntry{" +
"title='" + title + '\'' +
", description='" + description + '\'' +
", url='" + url + '\'' +
", imageUrl='" + imageUrl + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
NewsEntry newsEntry = (NewsEntry) o;
return Objects.equals(title, newsEntry.title) &&
Objects.equals(description, newsEntry.description) &&
Objects.equals(url, newsEntry.url) &&
Objects.equals(imageUrl, newsEntry.imageUrl);
}
@Override
public int hashCode() {
return Objects.hash(title, description, url, imageUrl);
}
}
}
API応答の共通構造
すべてのAPI呼び出しに対する共通の応答構造を定義します。これにより、エラー処理を一元化できます。
import java.util.Objects;
public class ApiResponse {
/**
* エラーコード
*/
private Integer errorCode;
/**
* エラーメッセージ
*/
private String errorMessage;
/**
* 成功応答かどうかを判定します。
*/
public boolean isSuccessful() {
return errorCode != null && errorCode == 0;
}
/**
* エラー応答かどうかを判定します。
*/
public boolean isError() {
return errorCode != null && errorCode != 0;
}
public Integer getErrorCode() {
return errorCode;
}
public void setErrorCode(Integer errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
@Override
public String toString() {
return "ApiResponse{" +
"errorCode=" + errorCode +
", errorMessage='" + errorMessage + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ApiResponse that = (ApiResponse) o;
return Objects.equals(errorCode, that.errorCode) &&
Objects.equals(errorMessage, that.errorMessage);
}
@Override
public int hashCode() {
return Objects.hash(errorCode, errorMessage);
}
}
グラフィックテンプレートカードメッセージ
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* グラフィック表示テンプレートカードメッセージのデータ構造
*/
public class GraphicCardMessage extends MessagePayload {
private final TemplateCardLayout templateCard;
private GraphicCardMessage(TemplateCardLayout cardLayout) {
super("template_card");
this.templateCard = cardLayout;
}
public TemplateCardLayout getTemplateCard() {
return templateCard;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
GraphicCardMessage that = (GraphicCardMessage) o;
return Objects.equals(templateCard, that.templateCard);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), templateCard);
}
@Override
public String toString() {
return "GraphicCardMessage(super=" + super.toString() +
", templateCard=" + templateCard + ")";
}
/**
* シンプルなグラフィックテンプレートカードメッセージを作成します。
*
* @param primaryTitle 主タイトル
* @param cardPicture カード画像
* @param cardInteraction カードクリック時の動作
* @return メッセージペイロード
*/
public static GraphicCardMessage createSimpleCard(PrimaryTitle primaryTitle, CardImageInfo cardPicture, CardInteraction cardInteraction) {
return newCardBuilder()
.withCardType("news_notice")
.withPrimaryTitle(primaryTitle)
.withCardImageInfo(cardPicture)
.withCardInteraction(cardInteraction)
.build();
}
/**
* テンプレートカードのビルダーを作成します。
*/
public static CardBuilder newCardBuilder() {
return new CardBuilder();
}
/**
* テンプレートカードメッセージのビルダー
*/
public static class CardBuilder {
private String cardType = "news_notice";
private SourceInfo sourceInformation;
private PrimaryTitle mainTitle;
private CardImageInfo cardImage;
private ImageTextBlock imageTextArea;
private QuoteBlock quoteArea;
private List<VerticalElement> verticalContentItems;
private List<HorizontalElement> horizontalContentItems;
private List<JumpAction> jumpActions;
private CardInteraction cardAction;
public CardBuilder withCardType(String type) {
this.cardType = type;
return this;
}
public CardBuilder withSourceInfo(SourceInfo info) {
this.sourceInformation = info;
return this;
}
public CardBuilder withPrimaryTitle(PrimaryTitle title) {
this.mainTitle = title;
return this;
}
public CardBuilder withCardImageInfo(CardImageInfo imageInfo) {
this.cardImage = imageInfo;
return this;
}
public CardBuilder withImageTextBlock(ImageTextBlock textBlock) {
this.imageTextArea = textBlock;
return this;
}
public CardBuilder withQuoteBlock(QuoteBlock block) {
this.quoteArea = block;
return this;
}
public CardBuilder withVerticalContentItems(List<VerticalElement> items) {
this.verticalContentItems = items;
return this;
}
public CardBuilder withHorizontalContentItems(List<HorizontalElement> items) {
this.horizontalContentItems = items;
return this;
}
public CardBuilder withJumpActions(List<JumpAction> actions) {
this.jumpActions = actions;
return this;
}
public CardBuilder withCardInteraction(CardInteraction action) {
this.cardAction = action;
return this;
}
/**
* テンプレートカードメッセージを構築します。
*/
public GraphicCardMessage build() {
// 必須条件の検証
if (mainTitle == null) {
throw new IllegalArgumentException("主タイトル (mainTitle) は必須です。");
}
if (cardImage == null) {
throw new IllegalArgumentException("カード画像 (cardImage) は必須です。");
}
if (cardAction == null) {
throw new IllegalArgumentException("ニュース通知テンプレートカードではカードインタラクション (cardAction) は必須です。");
}
if (imageTextArea != null && imageTextArea.getImageUrl() == null) {
throw new IllegalArgumentException("画像テキストブロック (imageTextArea) に画像URLは必須です。");
}
TemplateCardLayout cardLayout = new TemplateCardLayout(
cardType, sourceInformation, mainTitle, cardImage, imageTextArea, quoteArea,
verticalContentItems, horizontalContentItems, jumpActions, cardAction
);
return new GraphicCardMessage(cardLayout);
}
}
/**
* テンプレートカードのレイアウト定義
*/
public static class TemplateCardLayout {
/**
* テンプレートカードの種類 (グラフィック表示テンプレートは "news_notice")
*/
private final String card_type;
/**
* カードのソーススタイル情報 (不要な場合は省略可)
*/
private final SourceInfo source;
/**
* テンプレートカードの主要コンテンツ (一次タイトルと補助情報を含む)
*/
private final PrimaryTitle main_title;
/**
* 画像のスタイル
*/
private final CardImageInfo card_image;
/**
* 左画像右テキストのスタイル
*/
private final ImageTextBlock image_text_area;
/**
* 引用文献スタイル (キーデータとの併用は推奨されません)
*/
private final QuoteBlock quote_area;
/**
* カードの二次垂直コンテンツ (空配列でも可、データがある場合は対応するフィールドの必須性を確認。リスト長は4以下)
*/
private final List<VerticalElement> vertical_content_list;
/**
* 二次タイトル+テキストリスト (空配列でも可、データがある場合は対応するフィールドの必須性を確認。リスト長は6以下)
*/
private final List<HorizontalElement> horizontal_content_list;
/**
* ジャンプガイドスタイルのリスト (空配列でも可、データがある場合は対応するフィールドの必須性を確認。リスト長は3以下)
*/
private final List<JumpAction> jump_list;
/**
* カード全体のクリックジャンプイベント (news_noticeテンプレートカードでは必須)
*/
private final CardInteraction card_action;
public TemplateCardLayout(String cardType, SourceInfo source, PrimaryTitle mainTitle, CardImageInfo cardImage,
ImageTextBlock imageTextArea, QuoteBlock quoteArea,
List<VerticalElement> verticalContentItems,
List<HorizontalElement> horizontalContentItems,
List<JumpAction> jumpActions, CardInteraction cardAction) {
this.card_type = cardType;
this.source = source;
this.main_title = mainTitle;
this.card_image = cardImage;
this.image_text_area = imageTextArea;
this.quote_area = quoteArea;
this.vertical_content_list = verticalContentItems;
this.horizontal_content_list = horizontalContentItems;
this.jump_list = jumpActions;
this.card_action = cardAction;
}
public String getCard_type() { return card_type; }
public SourceInfo getSource() { return source; }
public PrimaryTitle getMain_title() { return main_title; }
public CardImageInfo getCard_image() { return card_image; }
public ImageTextBlock getImage_text_area() { return image_text_area; }
public QuoteBlock getQuote_area() { return quote_area; }
public List<VerticalElement> getVertical_content_list() { return vertical_content_list; }
public List<HorizontalElement> getHorizontal_content_list() { return horizontal_content_list; }
public List<JumpAction> getJump_list() { return jump_list; }
public CardInteraction getCard_action() { return card_action; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TemplateCardLayout that = (TemplateCardLayout) o;
return Objects.equals(card_type, that.card_type) &&
Objects.equals(source, that.source) &&
Objects.equals(main_title, that.main_title) &&
Objects.equals(card_image, that.card_image) &&
Objects.equals(image_text_area, that.image_text_area) &&
Objects.equals(quote_area, that.quote_area) &&
Objects.equals(vertical_content_list, that.vertical_content_list) &&
Objects.equals(horizontal_content_list, that.horizontal_content_list) &&
Objects.equals(jump_list, that.jump_list) &&
Objects.equals(card_action, that.card_action);
}
@Override
public int hashCode() {
return Objects.hash(card_type, source, main_title, card_image, image_text_area,
quote_area, vertical_content_list, horizontal_content_list,
jump_list, card_action);
}
@Override
public String toString() {
return "TemplateCardLayout{" +
"card_type='" + card_type + '\'' +
", source=" + source +
", main_title=" + main_title +
", card_image=" + card_image +
", image_text_area=" + image_text_area +
", quote_area=" + quote_area +
", vertical_content_list=" + vertical_content_list +
", horizontal_content_list=" + horizontal_content_list +
", jump_list=" + jump_list +
", card_action=" + card_action +
'}';
}
}
/**
* カードのソース情報
*/
public static class SourceInfo {
/**
* ソース画像のURL
*/
private final String icon_url;
/**
* ソース画像の記述 (13文字以内推奨)
*/
private final String desc;
/**
* ソーステキストの色 (0: デフォルト(灰色), 1: 黒, 2: 赤, 3: 緑 をサポート)
*/
private final Integer desc_color;
private SourceInfo(String iconUrl, String description, Integer descriptionColor) {
this.icon_url = iconUrl;
this.desc = description;
this.desc_color = descriptionColor;
}
public String getIcon_url() { return icon_url; }
public String getDesc() { return desc; }
public Integer getDesc_color() { return desc_color; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SourceInfo that = (SourceInfo) o;
return Objects.equals(icon_url, that.icon_url) &&
Objects.equals(desc, that.desc) &&
Objects.equals(desc_color, that.desc_color);
}
@Override
public int hashCode() {
return Objects.hash(icon_url, desc, desc_color);
}
@Override
public String toString() {
return "SourceInfo{" +
"icon_url='" + icon_url + '\'' +
", desc='" + desc + '\'' +
", desc_color=" + desc_color +
'}';
}
public static class Builder {
private String iconUrl;
private String description;
private Integer descriptionColor = 0;
public Builder withIconUrl(String url) {
this.iconUrl = url;
return this;
}
public Builder withDescription(String desc) {
this.description = desc;
return this;
}
public Builder withDescriptionColor(Integer color) {
this.descriptionColor = color;
return this;
}
public SourceInfo build() {
return new SourceInfo(iconUrl, description, descriptionColor);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* テンプレートカードの主要タイトルコンテンツ
*/
public static class PrimaryTitle {
/**
* 一次タイトル (26文字以内推奨)
*/
private final String title;
/**
* タイトルの補助情報 (30文字以内推奨)
*/
private final String desc;
private PrimaryTitle(String titleText, String descriptionText) {
this.title = titleText;
this.desc = descriptionText;
}
public String getTitle() { return title; }
public String getDesc() { return desc; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PrimaryTitle that = (PrimaryTitle) o;
return Objects.equals(title, that.title) &&
Objects.equals(desc, that.desc);
}
@Override
public int hashCode() {
return Objects.hash(title, desc);
}
@Override
public String toString() {
return "PrimaryTitle{" +
"title='" + title + '\'' +
", desc='" + desc + '\'' +
'}';
}
public static class Builder {
private String title;
private String desc;
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withDescription(String descriptionText) {
this.desc = descriptionText;
return this;
}
public PrimaryTitle build() {
return new PrimaryTitle(title, desc);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* カード画像情報
*/
public static class CardImageInfo {
/**
* 画像のURL
*/
private final String url;
/**
* 画像のアスペクト比 (2.25未満、1.3より大きく、デフォルト1.3)
*/
private final Float aspect_ratio;
private CardImageInfo(String imageUrl, Float aspectRatio) {
this.url = imageUrl;
this.aspect_ratio = aspectRatio;
}
public String getUrl() { return url; }
public Float getAspect_ratio() { return aspect_ratio; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CardImageInfo that = (CardImageInfo) o;
return Objects.equals(url, that.url) &&
Objects.equals(aspect_ratio, that.aspect_ratio);
}
@Override
public int hashCode() {
return Objects.hash(url, aspect_ratio);
}
@Override
public String toString() {
return "CardImageInfo{" +
"url='" + url + '\'' +
", aspect_ratio=" + aspect_ratio +
'}';
}
public static class Builder {
private String url;
private Float aspectRatio = 1.3f;
public Builder withUrl(String imageUrl) {
this.url = imageUrl;
return this;
}
public Builder withAspectRatio(Float ratio) {
this.aspectRatio = ratio;
return this;
}
public CardImageInfo build() {
return new CardImageInfo(url, aspectRatio);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 左画像右テキストブロックのスタイル
*/
public static class ImageTextBlock {
/**
* クリックイベントタイプ (0: なし, 1: URLへジャンプ, 2: ミニプログラムへジャンプ)
*/
private final Integer type;
/**
* クリックジャンプURL (typeが1の場合に必須)
*/
private final String url;
/**
* クリックジャンプ先ミニプログラムのAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* クリックジャンプ先ミニプログラムのページパス (typeが2の場合にオプション)
*/
private final String pagepath;
/**
* 左画像右テキストブロックのタイトル
*/
private final String title;
/**
* 左画像右テキストブロックの記述
*/
private final String desc;
/**
* 左画像右テキストブロックの画像URL
*/
private final String image_url;
private ImageTextBlock(Integer type, String url, String appId, String pagePath,
String titleText, String descriptionText, String imageUrl) {
this.type = type;
this.url = url;
this.appid = appId;
this.pagepath = pagePath;
this.title = titleText;
this.desc = descriptionText;
this.image_url = imageUrl;
}
public Integer getType() { return type; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
public String getTitle() { return title; }
public String getDesc() { return desc; }
public String getImage_url() { return image_url; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImageTextBlock that = (ImageTextBlock) o;
return Objects.equals(type, that.type) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath) &&
Objects.equals(title, that.title) &&
Objects.equals(desc, that.desc) &&
Objects.equals(image_url, that.image_url);
}
@Override
public int hashCode() {
return Objects.hash(type, url, appid, pagepath, title, desc, image_url);
}
@Override
public String toString() {
return "ImageTextBlock{" +
"type=" + type +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
", title='" + title + '\'' +
", desc='" + desc + '\'' +
", image_url='" + image_url + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String url;
private String appid;
private String pagepath;
private String title;
private String desc;
private String image_url;
public Builder withType(Integer clickType) {
this.type = clickType;
return this;
}
public Builder withUrl(String clickUrl) {
this.url = clickUrl;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withDescription(String descriptionText) {
this.desc = descriptionText;
return this;
}
public Builder withImageUrl(String imageUrl) {
this.image_url = imageUrl;
return this;
}
public ImageTextBlock build() {
return new ImageTextBlock(type, url, appid, pagepath, title, desc, image_url);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 引用文献スタイルのブロック
*/
public static class QuoteBlock {
/**
* クリックイベントタイプ (0: なし, 1: URLへジャンプ, 2: ミニプログラムへジャンプ)
*/
private final Integer type;
/**
* クリックジャンプURL (typeが1の場合に必須)
*/
private final String url;
/**
* クリックジャンプ先ミニプログラムのAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* クリックジャンプ先ミニプログラムのページパス (typeが2の場合にオプション)
*/
private final String pagepath;
/**
* 引用文献ブロックのタイトル
*/
private final String title;
/**
* 引用文献ブロックの引用テキスト
*/
private final String quote_text;
private QuoteBlock(Integer type, String url, String appId, String pagePath,
String titleText, String quoteText) {
this.type = type;
this.url = url;
this.appid = appId;
this.pagepath = pagePath;
this.title = titleText;
this.quote_text = quoteText;
}
public Integer getType() { return type; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
public String getTitle() { return title; }
public String getQuote_text() { return quote_text; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QuoteBlock that = (QuoteBlock) o;
return Objects.equals(type, that.type) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath) &&
Objects.equals(title, that.title) &&
Objects.equals(quote_text, that.quote_text);
}
@Override
public int hashCode() {
return Objects.hash(type, url, appid, pagepath, title, quote_text);
}
@Override
public String toString() {
return "QuoteBlock{" +
"type=" + type +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
", title='" + title + '\'' +
", quote_text='" + quote_text + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String url;
private String appid;
private String pagepath;
private String title;
private String quote_text;
public Builder withType(Integer clickType) {
this.type = clickType;
return this;
}
public Builder withUrl(String clickUrl) {
this.url = clickUrl;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withQuoteText(String quoteText) {
this.quote_text = quoteText;
return this;
}
public QuoteBlock build() {
return new QuoteBlock(type, url, appid, pagepath, title, quote_text);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 垂直コンテンツ要素
*/
public static class VerticalElement {
/**
* カードの二次タイトル (26文字以内推奨)
*/
private final String title;
/**
* 二次通常テキスト (112文字以内推奨)
*/
private final String desc;
private VerticalElement(String titleText, String descriptionText) {
this.title = titleText;
this.desc = descriptionText;
}
public String getTitle() { return title; }
public String getDesc() { return desc; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VerticalElement that = (VerticalElement) o;
return Objects.equals(title, that.title) &&
Objects.equals(desc, that.desc);
}
@Override
public int hashCode() {
return Objects.hash(title, desc);
}
@Override
public String toString() {
return "VerticalElement{" +
"title='" + title + '\'' +
", desc='" + desc + '\'' +
'}';
}
public static class Builder {
private String title;
private String desc;
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withDescription(String descriptionText) {
this.desc = descriptionText;
return this;
}
public VerticalElement build() {
return new VerticalElement(title, desc);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 水平コンテンツリストアイテム
*/
public static class HorizontalElement {
/**
* テンプレートカードの二次タイトル情報コンテンツタイプ (1: URL, 2: ファイル添付, 3: メンバー詳細へジャンプ)
*/
private final Integer type;
/**
* 二次タイトル (5文字以内推奨)
*/
private final String keyname;
/**
* 二次テキスト (typeが2の場合、ファイル名を含む。26文字以内推奨)
*/
private final String value;
/**
* リンクジャンプURL (typeが1の場合に必須)
*/
private final String url;
/**
* 添付ファイルのメディアID (typeが2の場合に必須)
*/
private final String media_id;
/**
* メンバー詳細のUserID (typeが3の場合に必須)
*/
private final String userid;
private HorizontalElement(Integer type, String keyName, String valueText,
String urlLink, String mediaId, String userId) {
this.type = type;
this.keyname = keyName;
this.value = valueText;
this.url = urlLink;
this.media_id = mediaId;
this.userid = userId;
}
public Integer getType() { return type; }
public String getKeyname() { return keyname; }
public String getValue() { return value; }
public String getUrl() { return url; }
public String getMedia_id() { return media_id; }
public String getUserid() { return userid; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HorizontalElement that = (HorizontalElement) o;
return Objects.equals(type, that.type) &&
Objects.equals(keyname, that.keyname) &&
Objects.equals(value, that.value) &&
Objects.equals(url, that.url) &&
Objects.equals(media_id, that.media_id) &&
Objects.equals(userid, that.userid);
}
@Override
public int hashCode() {
return Objects.hash(type, keyname, value, url, media_id, userid);
}
@Override
public String toString() {
return "HorizontalElement{" +
"type=" + type +
", keyname='" + keyname + '\'' +
", value='" + value + '\'' +
", url='" + url + '\'' +
", media_id='" + media_id + '\'' +
", userid='" + userid + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String keyname;
private String value;
private String url;
private String media_id;
private String userid;
public Builder withType(Integer elementType) {
this.type = elementType;
return this;
}
public Builder withKeyname(String key) {
this.keyname = key;
return this;
}
public Builder withValue(String val) {
this.value = val;
return this;
}
public Builder withUrl(String urlLink) {
this.url = urlLink;
return this;
}
public Builder withMediaId(String mediaId) {
this.media_id = mediaId;
return this;
}
public Builder withUserid(String userId) {
this.userid = userId;
return this;
}
public HorizontalElement build() {
return new HorizontalElement(type, keyname, value, url, media_id, userid);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* ジャンプガイドアイテム
*/
public static class JumpAction {
/**
* ジャンプリンクタイプ (0: 非リンク, 1: URLへジャンプ, 2: ミニプログラムへジャンプ)
*/
private final Integer type;
/**
* ジャンプリンクスタイルのテキストコンテンツ (13文字以内推奨)
*/
private final String title;
/**
* ジャンプリンクのURL (typeが1の場合に必須)
*/
private final String url;
/**
* ジャンプリンク先ミニプログラムのAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* ジャンプリンク先ミニプログラムのページパス (typeが2の場合にオプション)
*/
private final String pagepath;
private JumpAction(Integer type, String titleText, String urlLink, String appId, String pagePath) {
this.type = type;
this.title = titleText;
this.url = urlLink;
this.appid = appId;
this.pagepath = pagePath;
}
public Integer getType() { return type; }
public String getTitle() { return title; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JumpAction that = (JumpAction) o;
return Objects.equals(type, that.type) &&
Objects.equals(title, that.title) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath);
}
@Override
public int hashCode() {
return Objects.hash(type, title, url, appid, pagepath);
}
@Override
public String toString() {
return "JumpAction{" +
"type=" + type +
", title='" + title + '\'' +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String title;
private String url;
private String appid;
private String pagepath;
public Builder withType(Integer actionType) {
this.type = actionType;
return this;
}
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withUrl(String urlLink) {
this.url = urlLink;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public JumpAction build() {
return new JumpAction(type, title, url, appid, pagepath);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* カードクリック時のインタラクション
*/
public static class CardInteraction {
/**
* カードジャンプタイプ (1: URLへジャンプ, 2: ミニプログラムを開く。news_noticeテンプレートカードでは[1,2]の範囲)
*/
private final Integer type;
/**
* ジャンプイベントのURL (typeが1の場合に必須)
*/
private final String url;
/**
* ジャンプイベントのミニプログラムAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* ジャンプイベントのミニプログラムページパス (typeが2の場合にオプション)
*/
private final String pagepath;
private CardInteraction(Integer type, String urlLink, String appId, String pagePath) {
this.type = type;
this.url = urlLink;
this.appid = appId;
this.pagepath = pagePath;
}
public Integer getType() { return type; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CardInteraction that = (CardInteraction) o;
return Objects.equals(type, that.type) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath);
}
@Override
public int hashCode() {
return Objects.hash(type, url, appid, pagepath);
}
@Override
public String toString() {
return "CardInteraction{" +
"type=" + type +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
'}';
}
public static class Builder {
private Integer type;
private String url;
private String appid;
private String pagepath;
public Builder withType(Integer interactionType) {
this.type = interactionType;
return this;
}
public Builder withUrl(String urlLink) {
this.url = urlLink;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public CardInteraction build() {
return new CardInteraction(type, url, appid, pagepath);
}
}
public static Builder builder() {
return new Builder();
}
}
public static void main(String[] args) {
// シンプルなグラフィックテンプレートカードを作成する例
GraphicCardMessage.PrimaryTitle primaryTitle = GraphicCardMessage.PrimaryTitle.builder()
.withTitle("企業WeChatへようこそ")
.withDescription("友達があなたを企業WeChatに招待しています")
.build();
GraphicCardMessage.CardImageInfo cardImage = GraphicCardMessage.CardImageInfo.builder()
.withUrl("https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0")
.withAspectRatio(2.25f)
.build();
GraphicCardMessage.CardInteraction cardAction = GraphicCardMessage.CardInteraction.builder()
.withType(1)
.withUrl("https://work.weixin.qq.com/?from=openApi")
.build();
GraphicCardMessage simpleCard = GraphicCardMessage.createSimpleCard(primaryTitle, cardImage, cardAction);
System.out.println("Simple Card: " + simpleCard);
// 複雑なグラフィックテンプレートカードを作成する例
GraphicCardMessage.SourceInfo source = GraphicCardMessage.SourceInfo.builder()
.withIconUrl("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0")
.withDescription("企業WeChat")
.withDescriptionColor(0)
.build();
GraphicCardMessage.ImageTextBlock imageTextArea = GraphicCardMessage.ImageTextBlock.builder()
.withType(1)
.withUrl("https://work.weixin.qq.com")
.withTitle("企業WeChatへようこそ")
.withDescription("友達があなたを企業WeChatに招待しています")
.withImageUrl("https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0")
.build();
GraphicCardMessage.QuoteBlock quoteArea = GraphicCardMessage.QuoteBlock.builder()
.withType(1)
.withUrl("https://work.weixin.qq.com/?from=openApi")
.withTitle("引用テキストタイトル")
.withQuoteText("Jack:企業WeChatは本当に使いやすいです〜\nBalian:最高のソフトウェアです!")
.build();
List<VerticalElement> verticalContents = Arrays.asList(
GraphicCardMessage.VerticalElement.builder()
.withTitle("サプライズ紅包をGET")
.withDescription("企業WeChatをダウンロードして紅包をゲットしよう!")
.build()
);
List<HorizontalElement> horizontalContents = Arrays.asList(
GraphicCardMessage.HorizontalElement.builder()
.withKeyname("招待者")
.withValue("張三")
.build(),
GraphicCardMessage.HorizontalElement.builder()
.withType(1)
.withKeyname("企業WeChat公式サイト")
.withValue("アクセスする")
.withUrl("https://work.weixin.qq.com/?from=openApi")
.build(),
GraphicCardMessage.HorizontalElement.builder()
.withType(2)
.withKeyname("企業WeChatダウンロード")
.withValue("企業WeChat.apk")
.withMediaId("38Kz4vLCb49gfGvfOPw_y2f4nrik8lA_1xqNFmIzhtgkZVBoLoEYDQ7WopEKGnCTG")
.build()
);
List<JumpAction> jumpList = Arrays.asList(
GraphicCardMessage.JumpAction.builder()
.withType(1)
.withTitle("企業WeChat公式サイト")
.withUrl("https://work.weixin.qq.com/?from=openApi")
.build(),
GraphicCardMessage.JumpAction.builder()
.withType(2)
.withTitle("ミニプログラムへジャンプ")
.withAppid("APPID")
.withPagepath("PAGEPATH")
.build()
);
GraphicCardMessage complexCard = GraphicCardMessage.newCardBuilder()
.withSourceInfo(source)
.withPrimaryTitle(primaryTitle)
.withCardImageInfo(cardImage)
.withImageTextBlock(imageTextArea)
.withQuoteBlock(quoteArea)
.withVerticalContentItems(verticalContents)
.withHorizontalContentItems(horizontalContents)
.withJumpActions(jumpList)
.withCardInteraction(cardAction)
.build();
System.out.println("Complex Card: " + complexCard);
}
}
テキストテンプレートカードメッセージ
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* テキスト通知テンプレートカードメッセージのデータ構造
*/
public class TextCardMessage extends MessagePayload {
private final TemplateCardLayout templateCard;
private TextCardMessage(TemplateCardLayout cardLayout) {
super("template_card");
this.templateCard = cardLayout;
}
public TemplateCardLayout getTemplateCard() {
return templateCard;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
TextCardMessage that = (TextCardMessage) o;
return Objects.equals(templateCard, that.templateCard);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), templateCard);
}
@Override
public String toString() {
return "TextCardMessage(super=" + super.toString() +
", templateCard=" + templateCard + ")";
}
/**
* シンプルなテキスト通知テンプレートカードメッセージを作成します。
* @param primaryTitle 主タイトル
* @param cardInteraction カードクリック時の動作
* @return メッセージペイロード
*/
public static TextCardMessage createSimpleCard(PrimaryTitle primaryTitle, CardInteraction cardInteraction) {
return newCardBuilder()
.withCardType("text_notice")
.withPrimaryTitle(primaryTitle)
.withCardInteraction(cardInteraction)
.build();
}
/**
* テンプレートカードのビルダーを作成します。
*/
public static CardBuilder newCardBuilder() {
return new CardBuilder();
}
/**
* テンプレートカードメッセージのビルダー
*/
public static class CardBuilder {
private String cardType = "text_notice";
private SourceInfo sourceInformation;
private PrimaryTitle mainTitle;
private KeyData emphasisContent;
private QuoteBlock quoteArea;
private String subTitleText;
private List<HorizontalElement> horizontalContentItems;
private List<JumpAction> jumpActions;
private CardInteraction cardAction;
public CardBuilder withCardType(String type) {
this.cardType = type;
return this;
}
public CardBuilder withSourceInfo(SourceInfo info) {
this.sourceInformation = info;
return this;
}
public CardBuilder withPrimaryTitle(PrimaryTitle title) {
this.mainTitle = title;
return this;
}
public CardBuilder withEmphasisContent(KeyData content) {
this.emphasisContent = content;
return this;
}
public CardBuilder withQuoteArea(QuoteBlock block) {
this.quoteArea = block;
return this;
}
public CardBuilder withSubTitleText(String text) {
this.subTitleText = text;
return this;
}
public CardBuilder withHorizontalContentItems(List<HorizontalElement> items) {
this.horizontalContentItems = items;
return this;
}
public CardBuilder withJumpActions(List<JumpAction> actions) {
this.jumpActions = actions;
return this;
}
public CardBuilder withCardInteraction(CardInteraction action) {
this.cardAction = action;
return this;
}
/**
* テンプレートカードメッセージを構築します。
* 検証: primaryTitle.title または subTitleText のいずれかは必須です。
*/
public TextCardMessage build() {
// 必須条件の検証
if (mainTitle == null || (mainTitle.getTitle() == null && subTitleText == null)) {
throw new IllegalArgumentException("主タイトル (mainTitle.title) とサブタイトルテキスト (subTitleText) のいずれかは必須です。");
}
if (cardAction == null) {
throw new IllegalArgumentException("テキスト通知テンプレートカードではカードインタラクション (cardAction) は必須です。");
}
TemplateCardLayout cardLayout = new TemplateCardLayout(
cardType, sourceInformation, mainTitle, emphasisContent, quoteArea,
subTitleText, horizontalContentItems, jumpActions, cardAction
);
return new TextCardMessage(cardLayout);
}
}
/**
* テンプレートカードのレイアウト定義
*/
public static class TemplateCardLayout {
/**
* テンプレートカードの種類 (テキスト通知テンプレートは "text_notice")
*/
private final String card_type;
/**
* カードのソーススタイル情報 (不要な場合は省略可)
*/
private final SourceInfo source;
/**
* テンプレートカードの主要コンテンツ (一次タイトルと補助情報を含む)
*/
private final PrimaryTitle main_title;
/**
* キーデータスタイル
*/
private final KeyData emphasis_content;
/**
* 引用文献スタイル (キーデータとの併用は推奨されません)
*/
private final QuoteBlock quote_area;
/**
* 二次通常テキスト (112文字以内推奨)。
* テンプレートカード主要コンテンツの一次タイトル (main_title.title) と
* 二次通常テキスト (sub_title_text) のいずれかは必須です。
*/
private final String sub_title_text;
/**
* 二次タイトル+テキストリスト (空配列でも可、データがある場合は対応するフィールドの必須性を確認。リスト長は6以下)
*/
private final List<HorizontalElement> horizontal_content_list;
/**
* ジャンプガイドスタイルのリスト (空配列でも可、データがある場合は対応するフィールドの必須性を確認。リスト長は3以下)
*/
private final List<JumpAction> jump_list;
/**
* カード全体のクリックジャンプイベント (text_noticeテンプレートカードでは必須)
*/
private final CardInteraction card_action;
public TemplateCardLayout(String cardType, SourceInfo source, PrimaryTitle mainTitle,
KeyData emphasisContent, QuoteBlock quoteArea,
String subTitleText, List<HorizontalElement> horizontalContentItems,
List<JumpAction> jumpActions, CardInteraction cardAction) {
this.card_type = cardType;
this.source = source;
this.main_title = mainTitle;
this.emphasis_content = emphasisContent;
this.quote_area = quoteArea;
this.sub_title_text = subTitleText;
this.horizontal_content_list = horizontalContentItems;
this.jump_list = jumpActions;
this.card_action = cardAction;
}
public String getCard_type() { return card_type; }
public SourceInfo getSource() { return source; }
public PrimaryTitle getMain_title() { return main_title; }
public KeyData getEmphasis_content() { return emphasis_content; }
public QuoteBlock getQuote_area() { return quote_area; }
public String getSub_title_text() { return sub_title_text; }
public List<HorizontalElement> getHorizontal_content_list() { return horizontal_content_list; }
public List<JumpAction> getJump_list() { return jump_list; }
public CardInteraction getCard_action() { return card_action; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TemplateCardLayout that = (TemplateCardLayout) o;
return Objects.equals(card_type, that.card_type) &&
Objects.equals(source, that.source) &&
Objects.equals(main_title, that.main_title) &&
Objects.equals(emphasis_content, that.emphasis_content) &&
Objects.equals(quote_area, that.quote_area) &&
Objects.equals(sub_title_text, that.sub_title_text) &&
Objects.equals(horizontal_content_list, that.horizontal_content_list) &&
Objects.equals(jump_list, that.jump_list) &&
Objects.equals(card_action, that.card_action);
}
@Override
public int hashCode() {
return Objects.hash(card_type, source, main_title, emphasis_content, quote_area,
sub_title_text, horizontal_content_list, jump_list, card_action);
}
@Override
public String toString() {
return "TemplateCardLayout{" +
"card_type='" + card_type + '\'' +
", source=" + source +
", main_title=" + main_title +
", emphasis_content=" + emphasis_content +
", quote_area=" + quote_area +
", sub_title_text='" + sub_title_text + '\'' +
", horizontal_content_list=" + horizontal_content_list +
", jump_list=" + jump_list +
", card_action=" + card_action +
'}';
}
}
/**
* カードのソース情報 (GraphicCardMessageと共有)
*/
public static class SourceInfo {
/**
* ソース画像のURL
*/
private final String icon_url;
/**
* ソース画像の記述 (13文字以内推奨)
*/
private final String desc;
/**
* ソーステキストの色 (0: デフォルト(灰色), 1: 黒, 2: 赤, 3: 緑 をサポート)
*/
private final Integer desc_color;
private SourceInfo(String iconUrl, String description, Integer descriptionColor) {
this.icon_url = iconUrl;
this.desc = description;
this.desc_color = descriptionColor;
}
public String getIcon_url() { return icon_url; }
public String getDesc() { return desc; }
public Integer getDesc_color() { return desc_color; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SourceInfo that = (SourceInfo) o;
return Objects.equals(icon_url, that.icon_url) &&
Objects.equals(desc, that.desc) &&
Objects.equals(desc_color, that.desc_color);
}
@Override
public int hashCode() {
return Objects.hash(icon_url, desc, desc_color);
}
@Override
public String toString() {
return "SourceInfo{" +
"icon_url='" + icon_url + '\'' +
", desc='" + desc + '\'' +
", desc_color=" + desc_color +
'}';
}
public static class Builder {
private String iconUrl;
private String description;
private Integer descriptionColor = 0;
public Builder withIconUrl(String url) {
this.iconUrl = url;
return this;
}
public Builder withDescription(String desc) {
this.description = desc;
return this;
}
public Builder withDescriptionColor(Integer color) {
this.descriptionColor = color;
return this;
}
public SourceInfo build() {
return new SourceInfo(iconUrl, description, descriptionColor);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* テンプレートカードの主要タイトルコンテンツ (GraphicCardMessageと共有)
*/
public static class PrimaryTitle {
/**
* 一次タイトル (26文字以内推奨)。
* テンプレートカード主要コンテンツの一次タイトル (main_title.title) と
* 二次通常テキスト (sub_title_text) のいずれかは必須です。
*/
private final String title;
/**
* タイトルの補助情報 (30文字以内推奨)
*/
private final String desc;
private PrimaryTitle(String titleText, String descriptionText) {
this.title = titleText;
this.desc = descriptionText;
}
public String getTitle() { return title; }
public String getDesc() { return desc; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PrimaryTitle that = (PrimaryTitle) o;
return Objects.equals(title, that.title) &&
Objects.equals(desc, that.desc);
}
@Override
public int hashCode() {
return Objects.hash(title, desc);
}
@Override
public String toString() {
return "PrimaryTitle{" +
"title='" + title + '\'' +
", desc='" + desc + '\'' +
'}';
}
public static class Builder {
private String title;
private String desc;
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withDescription(String descriptionText) {
this.desc = descriptionText;
return this;
}
public PrimaryTitle build() {
return new PrimaryTitle(title, desc);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* キーデータスタイル
*/
public static class KeyData {
/**
* キーデータのデータ内容 (10文字以内推奨)
*/
private final String title;
/**
* キーデータの記述内容 (15文字以内推奨)
*/
private final String desc;
private KeyData(String titleText, String descriptionText) {
this.title = titleText;
this.desc = descriptionText;
}
public String getTitle() { return title; }
public String getDesc() { return desc; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
KeyData that = (KeyData) o;
return Objects.equals(title, that.title) &&
Objects.equals(desc, that.desc);
}
@Override
public int hashCode() {
return Objects.hash(title, desc);
}
@Override
public String toString() {
return "KeyData{" +
"title='" + title + '\'' +
", desc='" + desc + '\'' +
'}';
}
public static class Builder {
private String title;
private String desc;
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withDescription(String descriptionText) {
this.desc = descriptionText;
return this;
}
public KeyData build() {
return new KeyData(title, desc);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 引用文献スタイルのブロック (GraphicCardMessageと共有)
*/
public static class QuoteBlock {
/**
* クリックイベントタイプ (0: なし, 1: URLへジャンプ, 2: ミニプログラムへジャンプ)
*/
private final Integer type;
/**
* クリックジャンプURL (typeが1の場合に必須)
*/
private final String url;
/**
* クリックジャンプ先ミニプログラムのAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* クリックジャンプ先ミニプログラムのページパス (typeが2の場合にオプション)
*/
private final String pagepath;
/**
* 引用文献ブロックのタイトル
*/
private final String title;
/**
* 引用文献ブロックの引用テキスト
*/
private final String quote_text;
private QuoteBlock(Integer type, String url, String appId, String pagePath,
String titleText, String quoteText) {
this.type = type;
this.url = url;
this.appid = appId;
this.pagepath = pagePath;
this.title = titleText;
this.quote_text = quoteText;
}
public Integer getType() { return type; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
public String getTitle() { return title; }
public String getQuote_text() { return quote_text; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QuoteBlock that = (QuoteBlock) o;
return Objects.equals(type, that.type) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath) &&
Objects.equals(title, that.title) &&
Objects.equals(quote_text, that.quote_text);
}
@Override
public int hashCode() {
return Objects.hash(type, url, appid, pagepath, title, quote_text);
}
@Override
public String toString() {
return "QuoteBlock{" +
"type=" + type +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
", title='" + title + '\'' +
", quote_text='" + quote_text + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String url;
private String appid;
private String pagepath;
private String title;
private String quote_text;
public Builder withType(Integer clickType) {
this.type = clickType;
return this;
}
public Builder withUrl(String clickUrl) {
this.url = clickUrl;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withQuoteText(String quoteText) {
this.quote_text = quoteText;
return this;
}
public QuoteBlock build() {
return new QuoteBlock(type, url, appid, pagepath, title, quote_text);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 水平コンテンツリストアイテム (GraphicCardMessageと共有)
*/
public static class HorizontalElement {
/**
* テンプレートカードの二次タイトル情報コンテンツタイプ (1: URL, 2: ファイル添付, 3: メンバー詳細へジャンプ)
*/
private final Integer type;
/**
* 二次タイトル (5文字以内推奨)
*/
private final String keyname;
/**
* 二次テキスト (typeが2の場合、ファイル名を含む。26文字以内推奨)
*/
private final String value;
/**
* リンクジャンプURL (typeが1の場合に必須)
*/
private final String url;
/**
* 添付ファイルのメディアID (typeが2の場合に必須)
*/
private final String media_id;
/**
* メンバー詳細のUserID (typeが3の場合に必須)
*/
private final String userid;
private HorizontalElement(Integer type, String keyName, String valueText,
String urlLink, String mediaId, String userId) {
this.type = type;
this.keyname = keyName;
this.value = valueText;
this.url = urlLink;
this.media_id = mediaId;
this.userid = userId;
}
public Integer getType() { return type; }
public String getKeyname() { return keyname; }
public String getValue() { return value; }
public String getUrl() { return url; }
public String getMedia_id() { return media_id; }
public String getUserid() { return userid; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HorizontalElement that = (HorizontalElement) o;
return Objects.equals(type, that.type) &&
Objects.equals(keyname, that.keyname) &&
Objects.equals(value, that.value) &&
Objects.equals(url, that.url) &&
Objects.equals(media_id, that.media_id) &&
Objects.equals(userid, that.userid);
}
@Override
public int hashCode() {
return Objects.hash(type, keyname, value, url, media_id, userid);
}
@Override
public String toString() {
return "HorizontalElement{" +
"type=" + type +
", keyname='" + keyname + '\'' +
", value='" + value + '\'' +
", url='" + url + '\'' +
", media_id='" + media_id + '\'' +
", userid='" + userid + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String keyname;
private String value;
private String url;
private String media_id;
private String userid;
public Builder withType(Integer elementType) {
this.type = elementType;
return this;
}
public Builder withKeyname(String key) {
this.keyname = key;
return this;
}
public Builder withValue(String val) {
this.value = val;
return this;
}
public Builder withUrl(String urlLink) {
this.url = urlLink;
return this;
}
public Builder withMediaId(String mediaId) {
this.media_id = mediaId;
return this;
}
public Builder withUserid(String userId) {
this.userid = userId;
return this;
}
public HorizontalElement build() {
return new HorizontalElement(type, keyname, value, url, media_id, userid);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* ジャンプガイドアイテム (GraphicCardMessageと共有)
*/
public static class JumpAction {
/**
* ジャンプリンクタイプ (0: 非リンク, 1: URLへジャンプ, 2: ミニプログラムへジャンプ)
*/
private final Integer type;
/**
* ジャンプリンクスタイルのテキストコンテンツ (13文字以内推奨)
*/
private final String title;
/**
* ジャンプリンクのURL (typeが1の場合に必須)
*/
private final String url;
/**
* ジャンプリンク先ミニプログラムのAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* ジャンプリンク先ミニプログラムのページパス (typeが2の場合にオプション)
*/
private final String pagepath;
private JumpAction(Integer type, String titleText, String urlLink, String appId, String pagePath) {
this.type = type;
this.title = titleText;
this.url = urlLink;
this.appid = appId;
this.pagepath = pagePath;
}
public Integer getType() { return type; }
public String getTitle() { return title; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
JumpAction that = (JumpAction) o;
return Objects.equals(type, that.type) &&
Objects.equals(title, that.title) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath);
}
@Override
public int hashCode() {
return Objects.hash(type, title, url, appid, pagepath);
}
@Override
public String toString() {
return "JumpAction{" +
"type=" + type +
", title='" + title + '\'' +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
'}';
}
public static class Builder {
private Integer type = 0;
private String title;
private String url;
private String appid;
private String pagepath;
public Builder withType(Integer actionType) {
this.type = actionType;
return this;
}
public Builder withTitle(String titleText) {
this.title = titleText;
return this;
}
public Builder withUrl(String urlLink) {
this.url = urlLink;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public JumpAction build() {
return new JumpAction(type, title, url, appid, pagepath);
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* カードクリック時のインタラクション (GraphicCardMessageと共有)
*/
public static class CardInteraction {
/**
* カードジャンプタイプ (1: URLへジャンプ, 2: ミニプログラムを開く。text_noticeテンプレートカードでは[1,2]の範囲)
*/
private final Integer type;
/**
* ジャンプイベントのURL (typeが1の場合に必須)
*/
private final String url;
/**
* ジャンプイベントのミニプログラムAppID (typeが2の場合に必須)
*/
private final String appid;
/**
* ジャンプイベントのミニプログラムページパス (typeが2の場合にオプション)
*/
private final String pagepath;
private CardInteraction(Integer type, String urlLink, String appId, String pagePath) {
this.type = type;
this.url = urlLink;
this.appid = appId;
this.pagepath = pagePath;
}
public Integer getType() { return type; }
public String getUrl() { return url; }
public String getAppid() { return appid; }
public String getPagepath() { return pagepath; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CardInteraction that = (CardInteraction) o;
return Objects.equals(type, that.type) &&
Objects.equals(url, that.url) &&
Objects.equals(appid, that.appid) &&
Objects.equals(pagepath, that.pagepath);
}
@Override
public int hashCode() {
return Objects.hash(type, url, appid, pagepath);
}
@Override
public String toString() {
return "CardInteraction{" +
"type=" + type +
", url='" + url + '\'' +
", appid='" + appid + '\'' +
", pagepath='" + pagepath + '\'' +
'}';
}
public static class Builder {
private Integer type;
private String url;
private String appid;
private String pagepath;
public Builder withType(Integer interactionType) {
this.type = interactionType;
return this;
}
public Builder withUrl(String urlLink) {
this.url = urlLink;
return this;
}
public Builder withAppid(String appId) {
this.appid = appId;
return this;
}
public Builder withPagepath(String pagePath) {
this.pagepath = pagePath;
return this;
}
public CardInteraction build() {
return new CardInteraction(type, url, appid, pagepath);
}
}
public static Builder builder() {
return new Builder();
}
}
public static void main(String[] args) {
// シンプルなテキスト通知テンプレートカードを作成する例
TextCardMessage.PrimaryTitle mainTitle = TextCardMessage.PrimaryTitle.builder()
.withTitle("企業WeChatへようこそ")
.withDescription("友達があなたを企業WeChatに招待しています")
.build();
TextCardMessage.CardInteraction cardAction = TextCardMessage.CardInteraction.builder()
.withType(1)
.withUrl("https://work.weixin.qq.com/?from=openApi")
.build();
TextCardMessage simpleCard = TextCardMessage.createSimpleCard(mainTitle, cardAction);
System.out.println("Simple Text Card: " + simpleCard);
// 複雑なテキスト通知テンプレートカードを作成する例
TextCardMessage.SourceInfo source = TextCardMessage.SourceInfo.builder()
.withIconUrl("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0")
.withDescription("企業WeChat")
.withDescriptionColor(0)
.build();
TextCardMessage.KeyData emphasisContent = TextCardMessage.KeyData.builder()
.withTitle("100")
.withDescription("データ意味")
.build();
TextCardMessage.QuoteBlock quoteArea = TextCardMessage.QuoteBlock.builder()
.withType(1)
.withUrl("https://work.weixin.qq.com/?from=openApi")
.withTitle("引用テキストタイトル")
.withQuoteText("Jack:企業WeChatは本当に使いやすいです〜\nBalian:最高のソフトウェアです!")
.build();
List<HorizontalElement> horizontalContents = Arrays.asList(
TextCardMessage.HorizontalElement.builder()
.withKeyname("招待者")
.withValue("張三")
.build(),
TextCardMessage.HorizontalElement.builder()
.withType(1)
.withKeyname("企業WeChat公式サイト")
.withValue("アクセスする")
.withUrl("https://work.weixin.qq.com/?from=openApi")
.build(),
TextCardMessage.HorizontalElement.builder()
.withType(2)
.withKeyname("企業WeChatダウンロード")
.withValue("企業WeChat.apk")
.withMediaId("38Kz4vLCb49gfGvfOPw_y2f4nrik8lA_1xqNFmIzhtgkZVBoLoEYDQ7WopEKGnCTG")
.build()
);
List<JumpAction> jumpList = Arrays.asList(
TextCardMessage.JumpAction.builder()
.withType(1)
.withTitle("企業WeChat公式サイト")
.withUrl("https://work.weixin.qq.com/?from=openApi")
.build()
);
TextCardMessage complexCard = TextCardMessage.newCardBuilder()
.withSourceInfo(source)
.withPrimaryTitle(mainTitle)
.withEmphasisContent(emphasisContent)
.withQuoteArea(quoteArea)
.withSubTitleText("企業WeChatをダウンロードして紅包をゲットしよう!")
.withHorizontalContentItems(horizontalContents)
.withJumpActions(jumpList)
.withCardInteraction(cardAction)
.build();
System.out.println("Complex Text Card: " + complexCard);
}
}
テキストメッセージ
import java.util.List;
import java.util.Objects;
/**
* テキストタイプメッセージのデータ構造
*/
public class TextMessage extends MessagePayload {
private final TextContent text;
TextMessage(TextContent textData) {
super("text");
this.text = textData;
}
public TextContent getText() {
return text;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
TextMessage that = (TextMessage) o;
return Objects.equals(text, that.text);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), text);
}
@Override
public String toString() {
return "TextMessage(super=" + super.toString() + ", text=" + text + ")";
}
/**
* テキストメッセージを構築します。
* @param content テキスト内容
* @return
*/
public static TextMessage of(String content) {
return of(content, null, null);
}
/**
* テキストメッセージを構築します。
* @param content テキスト内容
* @param mentionedMobiles メンションする携帯電話番号のリスト (@allで全員をメンション)
* @return
*/
public static TextMessage of(String content, List<String> mentionedMobiles) {
return of(content, null, mentionedMobiles);
}
/**
* テキストメッセージを構築します。
* @param content テキスト内容
* @param mentionedUsers メンションするユーザーIDのリスト (@allで全員をメンション。ユーザーIDが不明な場合はmentionedMobilesを使用)
* @param mentionedMobiles メンションする携帯電話番号のリスト (@allで全員をメンション)
* @return
*/
public static TextMessage of(String content, List<String> mentionedUsers, List<String> mentionedMobiles) {
TextContent textContent = new TextContent(content);
textContent.setMentionedUsers(mentionedUsers);
textContent.setMentionedMobiles(mentionedMobiles);
return new TextMessage(textContent);
}
/**
* テキストメッセージの具体的な内容
*/
public static class TextContent {
/**
* テキスト内容 (最大2048バイト、UTF-8エンコード)
*/
private final String content;
/**
* メンションするユーザーIDのリスト (@allで全員をメンション)
*/
private List<String> mentioned_list;
/**
* メンションする携帯電話番号のリスト (@allで全員をメンション)
*/
private List<String> mentioned_mobile_list;
public TextContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public List<String> getMentionedUsers() {
return mentioned_list;
}
public List<String> getMentionedMobiles() {
return mentioned_mobile_list;
}
public void setMentionedUsers(List<String> mentionedUsers) {
this.mentioned_list = mentionedUsers;
}
public void setMentionedMobiles(List<String> mentionedMobiles) {
this.mentioned_mobile_list = mentionedMobiles;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TextContent that = (TextContent) o;
return Objects.equals(content, that.content) &&
Objects.equals(mentioned_list, that.mentioned_list) &&
Objects.equals(mentioned_mobile_list, that.mentioned_mobile_list);
}
@Override
public int hashCode() {
return Objects.hash(content, mentioned_list, mentioned_mobile_list);
}
@Override
public String toString() {
return "TextContent{" +
"content='" + content + '\'' +
", mentioned_list=" + mentioned_list +
", mentioned_mobile_list=" + mentioned_mobile_list +
'}';
}
}
}
メディアアップロード応答
import java.util.Objects;
public class MediaUploadResponse extends ApiResponse {
private String type;
private String media_id;
private String created_at;
public MediaUploadResponse(){}
public MediaUploadResponse(Integer errorCode, String errorMessage, String type, String mediaId, String createdAt) {
super(errorCode, errorMessage); // Call super constructor if ApiResponse also has one
this.type = type;
this.media_id = mediaId;
this.created_at = createdAt;
}
public String getMedia_id() {
return media_id;
}
public void setMedia_id(String mediaId) {
this.media_id = mediaId;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getCreated_at() {
return created_at;
}
public void setCreated_at(String createdAt) {
this.created_at = createdAt;
}
@Override
public String toString() {
return "MediaUploadResponse{" +
"errorCode=" + getErrorCode() +
", errorMessage='" + getErrorMessage() + '\'' +
", type='" + type + '\'' +
", media_id='" + media_id + '\'' +
", created_at='" + created_at + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
MediaUploadResponse that = (MediaUploadResponse) o;
return Objects.equals(type, that.type) &&
Objects.equals(media_id, that.media_id) &&
Objects.equals(created_at, that.created_at);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), type, media_id, created_at);
}
}
音声メッセージ
import java.util.Objects;
/**
* 音声タイプメッセージのデータ構造
*/
public class VoiceMessage extends MessagePayload {
private final VoiceContent voice;
private VoiceMessage(VoiceContent voiceData) {
super("voice");
this.voice = voiceData;
}
public VoiceContent getVoice() {
return voice;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
VoiceMessage that = (VoiceMessage) o;
return Objects.equals(voice, that.voice);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), voice);
}
@Override
public String toString() {
return "VoiceMessage(super=" + super.toString() +
", voice=" + voice + ")";
}
/**
* 音声メッセージを構築します。
* @param mediaId 音声ファイルのメディアID (アップロードAPIで取得)
*/
public static VoiceMessage of(String mediaId) {
if (mediaId == null || mediaId.trim().isEmpty()) {
throw new IllegalArgumentException("メディアIDは空であってはなりません");
}
return new VoiceMessage(new VoiceContent(mediaId));
}
/**
* 音声メッセージの具体的な内容
*/
public static class VoiceContent {
/**
* 音声ファイルのメディアID
*/
private final String media_id;
public VoiceContent(String mediaId) {
this.media_id = mediaId;
}
public String getMedia_id() {
return media_id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
VoiceContent that = (VoiceContent) o;
return Objects.equals(media_id, that.media_id);
}
@Override
public int hashCode() {
return Objects.hash(media_id);
}
@Override
public String toString() {
return "VoiceContent(media_id=" + media_id + ")";
}
}
}
HTTPクライアント実装
メッセージペイロードを定義した後、実際に企業WeChatのWebhook APIと通信するためのHTTPクライアントを実装します。この実装は、Apache HttpClientを使用し、接続プーリング、タイムアウト設定、SSL証明書検証のスキップ(開発環境向け)などを考慮しています。
依存関係
pom.xml に以下の依存関係を追加します。
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- Apache HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- Google Gson for JSON processing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version> <!-- 2025年時点の安定バージョンを推奨 -->
</dependency>
</dependencies>
HTTP通信ユーティリティ
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* シンプルなHTTP通信ユーティリティクラス
*/
public class SimpleHttpClient {
private SimpleHttpClient() {
// インスタンス化を防止
}
private final static Logger logger = LoggerFactory.getLogger(SimpleHttpClient.class);
/** 接続確立のタイムアウト (ミリ秒) */
private static final int CONNECT_TIMEOUT_MS = 3000;
/** リクエスト送信のタイムアウト (ミリ秒) */
private static final int REQUEST_TIMEOUT_MS = 30000;
/** データ読み取りのタイムアウト (ミリ秒) */
private static final int SOCKET_TIMEOUT_MS = 180000;
/** 最大再試行回数 */
private static final int MAX_RETRIES = 0; // Webhookは通常、べき等ではないため、再試行は無効にする
/** コネクションプール全体の最大接続数 */
private static final int MAX_TOTAL_CONNECTIONS = 100;
/** ルートごとの最大接続数 */
private static final int MAX_CONNECTIONS_PER_ROUTE = 5;
/** アイドル状態の接続を維持する時間 (秒) */
private static final long KEEP_ALIVE_DURATION_SEC = 60;
// スレッドセーフなHttpClientインスタンス
private static final CloseableHttpClient httpClientInstance;
static {
logger.info("HttpClientUtilityの初期化を開始します...");
try {
httpClientInstance = buildHttpClient();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
logger.error("HttpClientの初期化に失敗しました。", e);
throw new ExceptionInInitializerError(e);
}
logger.info("HttpClientUtilityの初期化が完了しました。");
}
/**
* HttpClientインスタンスを取得します。
* @return CloseableHttpClient
*/
public static CloseableHttpClient getHttpClient() {
return httpClientInstance;
}
/**
* すべてのHTTPS証明書を信頼するためのTrustManager
*/
static class InsecureTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) { /* 何もしない */ }
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) { /* 何もしない */ }
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
/**
* SSL証明書検証をスキップするSSLConnectionSocketFactoryを構築します。
* @return SSLConnectionSocketFactory
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
private static SSLConnectionSocketFactory createInsecureSslSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new InsecureTrustManager()}, null);
return new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
}
/**
* カスタムHttpClientを構築します。
*
* @return CloseableHttpClient
* @throws KeyManagementException
* @throws NoSuchAlgorithmException
*/
private static CloseableHttpClient buildHttpClient() throws KeyManagementException, NoSuchAlgorithmException {
Registry<ConnectionSocketFactory> socketFactoryRegistry =
RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", createInsecureSslSocketFactory()).build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
// Keep-Alive戦略
ConnectionKeepAliveStrategy keepAliveStrategy = (response, context) -> KEEP_ALIVE_DURATION_SEC * 1000;
return HttpClients.custom()
.setDefaultRequestConfig(buildRequestConfig())
.setConnectionManager(connectionManager)
// Webhookは冪等ではない場合が多いので、基本的に再試行はしない
.setRetryHandler(new DefaultHttpRequestRetryHandler(MAX_RETRIES, false))
.evictExpiredConnections()
.evictIdleConnections(KEEP_ALIVE_DURATION_SEC, TimeUnit.SECONDS)
.setKeepAliveStrategy(keepAliveStrategy)
.build();
}
/**
* RequestConfigを構築します。
*
* @return RequestConfig
*/
private static RequestConfig buildRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(CONNECT_TIMEOUT_MS)
.setConnectionRequestTimeout(REQUEST_TIMEOUT_MS)
.setSocketTimeout(SOCKET_TIMEOUT_MS)
.build();
}
/**
* HTTP POSTリクエストをJSON形式のボディで送信します。
* Content-Type: application/json; charset=UTF-8
*
* @param url リクエストURL
* @param jsonBody リクエストボディのJSON文字列
* @param headers リクエストヘッダーのマップ
* @return 応答ボディの文字列
*/
public static String sendJsonPost(String url, String jsonBody, Map<String, String> headers) {
validateUrl(url);
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));
httpPost.setHeader("Content-Type", "application/json;charset=utf8");
headers.forEach(httpPost::setHeader);
try (CloseableHttpResponse response = getHttpClient().execute(httpPost)) {
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
return EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
}
return null;
} catch (IOException e) {
logger.error("JSON POSTリクエストの実行に失敗しました: {} - {}", url, e.getMessage());
throw new RuntimeException("JSON POSTリクエスト失敗: " + e.getMessage(), e);
}
}
/**
* HTTP POSTリクエストをmultipart/form-data形式で送信します。
*
* @param url リクエストURL
* @param formParams フォームデータパラメータのマップ
* @param headers リクエストヘッダーのマップ
* @param file アップロードするファイル
* @param fileFieldName ファイルフィールド名(例: "media")
* @return 応答ボディの文字列
*/
public static String sendMultipartPost(String url, Map<String, String> formParams, Map<String, String> headers, File file, String fileFieldName) {
validateUrl(url);
HttpPost httpPost = new HttpPost(url);
MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
// ファイルを追加
if (file != null && file.exists()) {
multipartEntityBuilder.addBinaryBody(fileFieldName, file, ContentType.DEFAULT_BINARY, encodeFileName(file.getName()));
}
// その他のパラメータを追加 (UTF-8エンコーディングを指定)
ContentType textContentType = ContentType.create("text/plain", StandardCharsets.UTF_8);
formParams.forEach((key, value) -> multipartEntityBuilder.addTextBody(key, value, textContentType));
httpPost.setEntity(multipartEntityBuilder.build());
headers.forEach(httpPost::setHeader);
try (CloseableHttpResponse response = getHttpClient().execute(httpPost)) {
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
return EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
}
return null;
} catch (IOException e) {
logger.error("Multipart POSTリクエストの実行に失敗しました: {} - {}", url, e.getMessage());
throw new RuntimeException("Multipart POSTリクエスト失敗: " + e.getMessage(), e);
}
}
/**
* ファイル名をUTF-8でURLエンコードします。
* @param fileName エンコードするファイル名
* @return エンコードされたファイル名
*/
private static String encodeFileName(String fileName) {
try {
return URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
logger.warn("ファイル名のURLエンコードに失敗しました。ファイル名: {}", fileName, e);
return fileName; // エンコード失敗時は元のファイル名を返す
}
}
/**
* URLが有効かどうかを検証します。
* @param url 検証するURL
*/
private static void validateUrl(String url) {
if (StringUtils.isBlank(url)) {
logger.error("HTTPクライアントのリクエストURLは空であってはなりません。");
throw new IllegalArgumentException("リクエストURLが空です。");
}
}
/**
* リソースを安全に閉じます。
* @param closeable 閉じるリソース
*/
private static void closeResource(Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException e) {
logger.warn("リソースのクローズに失敗しました。", e);
}
}
}
企業WeChatボットサービスの実装
最後に、定義したインターフェースをHTTPクライアントユーティリティとGoogle Gsonライブラリを使用して実装します。
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
/**
* 企業WeChat Webhook APIのデフォルト実装サービス
*/
public class WeChatBotService implements WeChatBotApiClient {
private final String webhookBaseUrl;
private final Gson gson;
public WeChatBotService(String baseUrl) {
this.webhookBaseUrl = baseUrl;
this.gson = new GsonBuilder().disableHtmlEscaping().create(); // HTMLエスケープを無効にすることも検討
}
/**
* 指定されたペイロードに基づいてメッセージを送信します。
* @param botKey ボットのユニークな識別子
* @param payload 送信するメッセージのデータ構造
* @return APIからの応答
*/
@Override
public ApiResponse sendMessage(String botKey, MessagePayload payload) {
String requestUrl = webhookBaseUrl + "/send?key=" + botKey;
String jsonParams = gson.toJson(payload);
Map<String, String> headers = new HashMap<>();
String responseJson = SimpleHttpClient.sendJsonPost(requestUrl, jsonParams, headers);
return gson.fromJson(responseJson, ApiResponse.class);
}
/**
* JSON形式のメッセージ文字列を直接送信します。
* @param botKey ボットのユニークな識別子
* @param jsonMessage 送信するメッセージのJSON文字列
* @return APIからの応答
*/
@Override
public ApiResponse sendMessage(String botKey, String jsonMessage) {
String requestUrl = webhookBaseUrl + "/send?key=" + botKey;
Map<String, String> headers = new HashMap<>();
String responseJson = SimpleHttpClient.sendJsonPost(requestUrl, jsonMessage, headers);
return gson.fromJson(responseJson, ApiResponse.class);
}
/**
* ファイルまたは音声をアップロードします。
* @param botKey ボットのユニークな識別子
* @param mediaType ファイルの種類 ("file" または "voice")
* @param file アップロードするファイル
* @return アップロードAPIからの応答
*/
@Override
public MediaUploadResponse uploadMedia(String botKey, String mediaType, File file) {
String requestUrl = webhookBaseUrl + "/upload_media?key=" + botKey + "&type=" + mediaType;
HashMap<String, String> formParams = new HashMap<>();
// フォームパラメータとしてkeyとtypeを送信する必要がある場合
formParams.put("key", botKey);
formParams.put("type", mediaType);
HashMap<String, String> headers = new HashMap<>();
String responseJson = SimpleHttpClient.sendMultipartPost(requestUrl, formParams, headers, file, "media");
// 例: {"errcode":0,"errmsg":"ok","type":"file","media_id":"...", "created_at":"..."}
return gson.fromJson(responseJson, MediaUploadResponse.class);
}
}
これらのコンポーネントを組み合わせることで、Spring Bootアプリケーションから企業WeChatグループボットへの柔軟なメッセージ通知システムを構築できます。