diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..900370b --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# traQ BOTサーバーライブラリ +traQ BOTサーバーを簡単に作るためのライブラリです。 + +`traQ--->BOTサーバー`へのイベントの受け取り部分を補助します。`BOTサーバー--->traQ`へのリクエストを行うための、traQ APIクライアントは含まれていません。 + +## サンプル +```go +package main + +import ( + "github.com/traPtitech/traq-bot" + "log" + "os" +) + +func main() { + vt := os.Getenv("VERIFICATION_TOKEN") + + handlers := traqbot.EventHandlers{} + handlers.SetMessageCreatedHandler(func(payload *traqbot.MessageCreatedPayload) { + log.Println("=================================================") + log.Printf("%sさんがメッセージを投稿しました。\n", payload.Message.User.DisplayName) + log.Println("メッセージ:") + log.Println(payload.Message.Text) + log.Println("=================================================") + }) + + server := traqbot.NewBotServer(vt, handlers) + log.Fatal(server.ListenAndServe(":80")) +} + +``` diff --git a/common_payload.go b/common_payload.go new file mode 100644 index 0000000..8b80d7a --- /dev/null +++ b/common_payload.go @@ -0,0 +1,73 @@ +package traqbot + +import "time" + +// BasePayload ベースペイロード +type BasePayload struct { + // EventTime イベント発生日時 + EventTime time.Time `json:"eventTime"` +} + +// UserPayload ユーザー情報ペイロード +type UserPayload struct { + // ID ユーザーUUID + ID string `json:"id"` + // Name ユーザーのtraQ ID + Name string `json:"name"` + // DisplayName ユーザーの表示名 + DisplayName string `json:"displayName"` + // IconID ユーザーアイコンのファイルUUID + IconID string `json:"iconId"` + // Bot ユーザーがBotかどうか + Bot bool `json:"bot"` +} + +// ChannelPayload チャンネル情報ペイロード +type ChannelPayload struct { + // ID チャンネルUUID + ID string `json:"id"` + // Name チャンネル名 + Name string `json:"name"` + // Path チャンネルパス + Path string `json:"path"` + // ParentID 親チャンネルのUUID + // + // ルートチャンネルの場合は"00000000-0000-0000-0000-000000000000" + ParentID string `json:"parentId"` + // Creator チャンネル作成者 + Creator UserPayload `json:"creator"` + // CreatedAt チャンネル作成日時 + CreatedAt time.Time `json:"createdAt"` + // UpdatedAt チャンネル更新日時 + UpdatedAt time.Time `json:"updatedAt"` +} + +// MessagePayload メッセージ情報ペイロード +type MessagePayload struct { + // ID メッセージUUID + ID string `json:"id"` + // User メッセージ投稿者 + User UserPayload `json:"user"` + // ChannelID 投稿先チャンネルUUID + ChannelID string `json:"channelId"` + // Text 生メッセージ本文 + Text string `json:"text"` + // PlainText メッセージ本文(埋め込み情報・改行なし) + PlainText string `json:"plainText"` + // Embedded メッセージ埋め込み情報の配列 + Embedded []EmbeddedInfoPayload `json:"embedded"` + // CreatedAt メッセージ投稿日時 + CreatedAt time.Time `json:"createdAt"` + // UpdatedAt メッセージ更新日時 + UpdatedAt time.Time `json:"updatedAt"` +} + +// EmbeddedInfoPayload メッセージ埋め込み情報 +type EmbeddedInfoPayload struct { + // Raw 表示文字列 + Raw string `json:"raw"` + // Type タイプ + Type string `json:"type"` + // ID 各種ID(タイプによる) + ID string `json:"id"` +} diff --git a/event_payload.go b/event_payload.go new file mode 100644 index 0000000..5924f4d --- /dev/null +++ b/event_payload.go @@ -0,0 +1,48 @@ +package traqbot + +// PingPayload PINGイベントペイロード +type PingPayload struct { + BasePayload +} + +// JoinedPayload JOINEDイベントペイロード +type JoinedPayload struct { + BasePayload + // Channel 参加したチャンネル + Channel ChannelPayload `json:"channel"` +} + +// LeftPayload LEFTイベントペイロード +type LeftPayload struct { + BasePayload + // Channel 退出したチャンネル + Channel ChannelPayload `json:"channel"` +} + +// MessageCreatedPayload MESSAGE_CREATEDイベントペイロード +type MessageCreatedPayload struct { + BasePayload + // Message 投稿されたメッセージ + Message MessagePayload `json:"message"` +} + +// DirectMessageCreatedPayload DIRECT_MESSAGE_CREATEDイベントペイロード +type DirectMessageCreatedPayload struct { + BasePayload + // Message 投稿されたメッセージ + Message MessagePayload `json:"message"` +} + +// ChannelCreatedPayload CHANNEL_CREATEDイベントペイロード +type ChannelCreatedPayload struct { + BasePayload + // Channel 作成されたチャンネル + Channel ChannelPayload `json:"channel"` +} + +// UserCreatedPayload USER_CREATEDイベントペイロード +type UserCreatedPayload struct { + BasePayload + // User 作成されたユーザー + User UserPayload `json:"user"` +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..da06cb8 --- /dev/null +++ b/events.go @@ -0,0 +1,18 @@ +package traqbot + +const ( + // Ping PINGイベント + Ping = "PING" + // Joined JOINEDイベント + Joined = "JOINED" + // Left LEFTイベント + Left = "LEFT" + // MessageCreated MESSAGE_CREATEDイベント + MessageCreated = "MESSAGE_CREATED" + // DirectMessageCreated DIRECT_MESSAGE_CREATEDイベント + DirectMessageCreated = "DIRECT_MESSAGE_CREATED" + // ChannelCreated CHANNEL_CREATEDイベント + ChannelCreated = "CHANNEL_CREATED" + // UserCreated USER_CREATEDイベント + UserCreated = "USER_CREATED" +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8c5c798 --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/traPtitech/traq-bot diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..43412e2 --- /dev/null +++ b/helper.go @@ -0,0 +1,47 @@ +package traqbot + +// SetHandler イベントハンドラをセットします +// +// ハンドラにnilを指定すると、ハンドラは削除されます +func (hs EventHandlers) SetHandler(event string, h EventHandler) { + if h == nil { + delete(hs, event) + } else { + hs[event] = h + } +} + +// SetPingHandler イベントハンドラをセットします +func (hs EventHandlers) SetPingHandler(h func(payload *PingPayload)) { + hs.SetHandler(Ping, func(event string, payload interface{}) { h(payload.(*PingPayload)) }) +} + +// SetJoinedHandler イベントハンドラをセットします +func (hs EventHandlers) SetJoinedHandler(h func(payload *JoinedPayload)) { + hs.SetHandler(Joined, func(event string, payload interface{}) { h(payload.(*JoinedPayload)) }) +} + +// SetLeftHandler イベントハンドラをセットします +func (hs EventHandlers) SetLeftHandler(h func(payload *LeftPayload)) { + hs.SetHandler(Left, func(event string, payload interface{}) { h(payload.(*LeftPayload)) }) +} + +// SetMessageCreatedHandler イベントハンドラをセットします +func (hs EventHandlers) SetMessageCreatedHandler(h func(payload *MessageCreatedPayload)) { + hs.SetHandler(MessageCreated, func(event string, payload interface{}) { h(payload.(*MessageCreatedPayload)) }) +} + +// SetDirectMessageCreatedHandler イベントハンドラをセットします +func (hs EventHandlers) SetDirectMessageCreatedHandler(h func(payload *DirectMessageCreatedPayload)) { + hs.SetHandler(DirectMessageCreated, func(event string, payload interface{}) { h(payload.(*DirectMessageCreatedPayload)) }) +} + +// SetChannelCreatedHandler イベントハンドラをセットします +func (hs EventHandlers) SetChannelCreatedHandler(h func(payload *ChannelCreatedPayload)) { + hs.SetHandler(ChannelCreated, func(event string, payload interface{}) { h(payload.(*ChannelCreatedPayload)) }) +} + +// SetUserCreatedHandler イベントハンドラをセットします +func (hs EventHandlers) SetUserCreatedHandler(h func(payload *UserCreatedPayload)) { + hs.SetHandler(UserCreated, func(event string, payload interface{}) { h(payload.(*UserCreatedPayload)) }) +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..94420d6 --- /dev/null +++ b/server.go @@ -0,0 +1,96 @@ +package traqbot + +import ( + "encoding/json" + "net/http" + "strings" +) + +const ( + contentTypeHeader = "Content-Type" + mimeApplicationJson = "application/json" + botTokenHeader = "X-TRAQ-BOT-TOKEN" + botEventHeader = "X-TRAQ-BOT-EVENT" +) + +// EventHandler イベントハンドラ +type EventHandler func(event string, payload interface{}) + +// EventHandlers イベントハンドラマップ +type EventHandlers map[string]EventHandler + +// BotServer BOTサーバー +type BotServer struct { + verificationToken string + handlers EventHandlers +} + +// NewBotServer 新しくBOTサーバーを生成します +func NewBotServer(verification string, handlers EventHandlers) *BotServer { + return &BotServer{ + verificationToken: verification, + handlers: handlers, + } +} + +// ListenAndServe BOTサーバーを起動します +func (bs *BotServer) ListenAndServe(addr string) error { + mux := http.NewServeMux() + mux.Handle("/", bs) + server := &http.Server{Addr: addr, Handler: mux} + return server.ListenAndServe() +} + +// ServeHTTP BOTサーバーハンドラ +func (bs *BotServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + // VerificationTokenチェック + if req.Header.Get(botTokenHeader) != bs.verificationToken { + rw.WriteHeader(http.StatusForbidden) + return + } + + // Eventヘッダチェック + event := req.Header.Get(botEventHeader) + if len(event) == 0 { + rw.WriteHeader(http.StatusBadRequest) + return + } + + // RequestがJSONかどうか + if !strings.HasPrefix(req.Header.Get(contentTypeHeader), mimeApplicationJson) { + rw.WriteHeader(http.StatusBadRequest) + return + } + + var payload interface{} + switch event { + case Ping: + payload = &PingPayload{} + case Joined: + payload = &JoinedPayload{} + case Left: + payload = &LeftPayload{} + case MessageCreated: + payload = &MessageCreatedPayload{} + case DirectMessageCreated: + payload = &DirectMessageCreatedPayload{} + case ChannelCreated: + payload = &ChannelCreatedPayload{} + case UserCreated: + payload = &UserCreatedPayload{} + default: + rw.WriteHeader(http.StatusNotImplemented) + return + } + + if err := json.NewDecoder(req.Body).Decode(payload); err != nil { + rw.WriteHeader(http.StatusBadRequest) + return + } + rw.WriteHeader(http.StatusNoContent) + + h, ok := bs.handlers[event] + if ok && h != nil { + h(event, payload) + } +}