From 4d02ae800f62cbc8fd5416c4b8d197fa37667f7a Mon Sep 17 00:00:00 2001 From: Bren Briggs Date: Tue, 23 Jan 2024 06:09:33 +0000 Subject: [PATCH] HIERARCHICAL TOPICS MEANS NO MORE METADATA --- examples/pingpong/main.go | 140 ------------- examples/py-pingpong/pingpong.py | 86 -------- examples/py-pingpong/requirements.txt | 6 - fly.toml | 31 +++ heroku.yml | 8 - messageCreateHandler.go | 16 +- model/message.go | 94 --------- model/message_test.go | 252 ---------------------- model/replies.go | 142 ------------- model/replies_test.go | 288 -------------------------- 10 files changed, 44 insertions(+), 1019 deletions(-) delete mode 100644 examples/pingpong/main.go delete mode 100755 examples/py-pingpong/pingpong.py delete mode 100644 examples/py-pingpong/requirements.txt create mode 100644 fly.toml delete mode 100644 heroku.yml delete mode 100644 model/message.go delete mode 100644 model/message_test.go delete mode 100644 model/replies.go delete mode 100644 model/replies_test.go diff --git a/examples/pingpong/main.go b/examples/pingpong/main.go deleted file mode 100644 index 3c0e94e..0000000 --- a/examples/pingpong/main.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "context" - "flag" - "os" - "strings" - "time" - - "github.com/bytebot-chat/gateway-discord/model" - "github.com/go-redis/redis/v8" - "github.com/rs/zerolog/log" -) - -const APP_NAME = "pingpong" - -var ( - addr = flag.String("redis", "localhost:6379", "Redis server address") - inbound = flag.String("inbound", "discord-inbound", "Pubsub queue to listen for new messages") - outbound = flag.String("outbound", "discord-outbound", "Pubsub queue for sending messages outbound") -) - -func init() { - flag.Parse() -} - -func main() { - - // An example of logging using zerolog - log.Info(). - Str("version", "0.0.1"). - Str("addr", *addr). - Str("inbound", *inbound). - Str("outbound", *outbound). - Msg("Starting pingpong") - - // Create a new Redis client - ctx := context.Background() // Redis context used for all Redis operations - log.Info(). - Str("address", *addr). - Msg("Connecting to redis") - - rdb := redis.NewClient(&redis.Options{ - Addr: *addr, - DB: 0, // use default DB - }) - - err := rdb.Ping(ctx).Err() // Ping redis to make sure it's up - - // If there is an error, log it and exit - if err != nil { - log.Err(err). - Str("address", *addr). - Msg("Unable to continue without connection. Trying again in 3 seconds") - time.Sleep(3 * time.Second) // Wait 3 seconds before trying again - err := rdb.Ping(ctx).Err() - if err != nil { - log.Err(err). - Str("address", *addr). - Msg("Unable to continue without connection. Exiting!") - os.Exit(1) - } - } - - // Messages come in from a pubsub queue - // We need to subscribe to the queue and listen for messages - // We use topic.Channel() to get the channel to listen on for messages and then use a for loop to listen for messages - // When a message comes in, we need to parse it and then send a response - // We use the MessageSend struct to send the response - // We then use the rdb.Publish() method to send the response to the outbound queue - - // Subscribe to the inbound queue - topic := rdb.Subscribe(ctx, *inbound) - channel := topic.Channel() - - // Listen for messages - for msg := range channel { - log.Debug(). - Msg("Received message") - - // Create a new MessageSend struct - var message model.Message - - // Unmarshal the message bytes into the struct - err := message.UnmarshalJSON([]byte(msg.Payload)) - - // If there is an error, log it and continue - if err != nil { - log.Err(err). - Msg("Unable to parse message") - - // If we can't parse the message, we can't send a response - // So we just continue to the next message - continue - } - - // Check if the message is a ping - if !strings.HasPrefix(message.Content, "ping") { - // If it's not a ping, we don't need to respond - continue - } - - // Log that we're sending a response - log.Info(). - Msg("Ping received. Sending pong") - - // Use a convenience method to create a new MessageSend struct - // This method takes the app name, the content of the message to send, whether to reply to the message, and whether to mention the user who sent the message - resp := message.RespondToChannelOrThread(APP_NAME, "pong from golang", true, false) - - // Debug log the response - log.Debug(). - Str("message", resp.Content). - Str("dest", resp.Metadata.Dest). - Msg("Sending message") - - // Marshal the struct into bytes - respBytes, err := resp.MarshalJSON() - - // If there is an error, log it and continue - if err != nil { - log.Err(err). - Str("message", msg.Payload). - Msg("Unable to marshal message") - continue - } - - // Publish the message to the outbound queue - err = rdb.Publish(ctx, *outbound, respBytes).Err() - - // If there is an error, log it and continue - if err != nil { - log.Err(err). - Str("message", msg.Payload). - Msg("Unable to publish message") - continue - } - } - -} diff --git a/examples/py-pingpong/pingpong.py b/examples/py-pingpong/pingpong.py deleted file mode 100755 index c9ebc49..0000000 --- a/examples/py-pingpong/pingpong.py +++ /dev/null @@ -1,86 +0,0 @@ -#! /usr/bin/env python -import argparse -import logging -import redis -import uuid -import json - - -def respondToChannelOrThread(message, source, content, should_reply=False, should_mention=False): - # debug print keys in message - # for key in message: - # logging.info("Key: %s, Value: %s\n", key, message[key]) - - msg = dict({}) - metadata = dict({}) - # source of the message is the name of the bot/app - metadata['source'] = source - # destination is the source of the message - metadata['dest'] = message['metadata']['source'] - # generate a random UUID for the message, must be a string - metadata['id'] = str(uuid.uuid4()) - # add the empty metadata dict to the message dict as the metadata key - msg['metadata'] = metadata - - # reply to the same channel as the message - msg['channel_id'] = message['message']['channel_id'] - # set the content of the message to the content argument - msg['content'] = content - # set the previous message to the message that triggered the response. Discord uses this for context when replying to a message. - msg['previous_message'] = message['message'] - # should_reply is a boolean that tells Discord whether or not to reply to the message that triggered the response. If should_reply is true, Discord will reply to the message that triggered the response. If should_reply is false, Discord will send the response as a new message in the channel. - msg['should_reply'] = should_reply - # should_mention is a boolean that tells Discord whether or not to mention the user that triggered the response. If should_mention is true, Discord will mention the user that triggered the response. If should_mention is false, Discord will not mention the user that triggered the response. - msg['should_mention'] = should_mention - - return msg - - -def main(): - # Parse command line arguments - parser = argparse.ArgumentParser(description='Ping Pong') - parser.add_argument("-r", "--redis", type=str, - default="localhost", help="Redis server address") - parser.add_argument("-p", "--port", type=int, - default=6379, help="Redis server port") - parser.add_argument('-i', '--inbound', type=str, - default='discord-inbound', help='Inbound queue name') - parser.add_argument('-o', '--outbound', type=str, - default='discord-outbound', help='Outbound queue name') - - args = parser.parse_args() - - # Configure logger - logging.basicConfig(level=logging.INFO) - logging.info("Starting ping pong") - - # Connect to Redis - r = redis.Redis(host=args.redis, port=6379, db=0) - p = r.pubsub() - p.subscribe(args.inbound) - - # Main loop - while True: - message = p.get_message() - if message and message['type'] == 'message': - # logging.info("Received message: %s", message['data']) - - # Parse message - msg = json.loads(message['data']) - - # for key in msg: - # logging.info("Key: %s, Value: %s\n", key, msg[key]) - - # Check if it's a ping - if msg['message']['content'] == 'ping': - # Respond with a pong - logging.info("Responding to ping") - # respond to the channel or thread that the message was sent in - pong = respondToChannelOrThread( - msg, 'python-pingpong', 'pong from python', should_reply=False, should_mention=True) - # send the json message to the outbound queue - r.publish(args.outbound, json.dumps(pong)) - - -if __name__ == "__main__": - main() diff --git a/examples/py-pingpong/requirements.txt b/examples/py-pingpong/requirements.txt deleted file mode 100644 index 81a6b1e..0000000 --- a/examples/py-pingpong/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -async-timeout==4.0.2 -Deprecated==1.2.13 -packaging==21.3 -pyparsing==3.0.9 -redis==4.3.4 -wrapt==1.14.1 diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..bd04010 --- /dev/null +++ b/fly.toml @@ -0,0 +1,31 @@ +# fly.toml app configuration file generated for bytebot-discord-gateway on 2023-12-08T05:05:12Z +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = "bytebot-discord-gateway" +primary_region = "atl" +kill_signal = "SIGINT" +kill_timeout = "5s" + +[build] + dockerfile = "Dockerfile" + +[env] + # Pattern is .. + BYTEBOT_ID = "discord.sithmail.prod" + # Pattern is ... + BYTEBOT_INBOUND = "inbound.discord.sithmail.prod" + # Pattern is ... + BYTEBOT_OUTBOUND = "outbound.discord.sithmail.prod" + REDIS_URL = "fly-bytebot.upstash.io:6379" + BYTEBOT_LOG_LEVEL = "DEBUG" + +[[vm]] + cpu_kind = "shared" + cpus = 1 + memory_mb = 1024 + +[[metrics]] + port = 8080 + path = "/metrics" diff --git a/heroku.yml b/heroku.yml deleted file mode 100644 index c0eb582..0000000 --- a/heroku.yml +++ /dev/null @@ -1,8 +0,0 @@ -# https://devcenter.heroku.com/articles/heroku-yml-build-manifest -# Officially unsupported, but works. -build: - docker: - worker: Dockerfile - -run: - worker: /opt/bytebot diff --git a/messageCreateHandler.go b/messageCreateHandler.go index 4c6b174..76ba400 100644 --- a/messageCreateHandler.go +++ b/messageCreateHandler.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "github.com/bwmarrin/discordgo" "github.com/rs/zerolog/log" ) @@ -22,11 +24,19 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { } // Topic ID is derived from the protocol, server, channel, and user and dot-delimited - // Example for sithmail: discord.sithmail.#channel.user - topic := "discord." + m.GuildID + "." + m.ChannelID + "." + m.Author.ID + // Example for sithmail: inbound.discord.sithmail.#channel.user + topic := "inbound.discord." + m.GuildID + "." + m.ChannelID + "." + m.Author.ID + + // Marshal the message into a byte array + // This is required because Redis only accepts byte arrays + jsonBytes, err := json.Marshal(m) + if err != nil { + log.Err(err).Msg("Failed to marshal message into JSON") + return + } // Publish the message to redis - res := rdb.Publish(redisCtx, topic, m) + res := rdb.Publish(redisCtx, topic, jsonBytes) if res.Err() != nil { log.Err(res.Err()).Msg("Unable to publish message") return diff --git a/model/message.go b/model/message.go deleted file mode 100644 index 551c030..0000000 --- a/model/message.go +++ /dev/null @@ -1,94 +0,0 @@ -package model - -import ( - "encoding/json" - - "github.com/bwmarrin/discordgo" - uuid "github.com/satori/go.uuid" -) - -// Message is the struct that is used to pass messages from the Gateway to the Redis pubsub (inbound messages) -type Message struct { - *discordgo.Message `json:"message,omitempty"` - Metadata Metadata `json:"metadata"` -} - -// Metadata is used by the Gateway(s) and app(s) to trace messages and identify intended recipients -type Metadata struct { - Source string `json:"source,omitempty"` // Source is the ID of the Gateway or App that sent the message - Dest string `json:"dest,omitempty"` // Dest is the ID of the Gateway or App that the message is intended for - ID uuid.UUID `json:"id,omitempty"` // ID is a UUID that is generated for each message -} - -// Marhsal converts the message to JSON -func (m *Message) Marshal() ([]byte, error) { - return json.Marshal(m) -} - -// Unmarshal converts the JSON (in bytes) to a message -// This method is deprecated in favor of the UnmarshalJSON method and will be removed in a future release -// Correct behavior from this method is not guaranteed -// Example: -// msg := &model.Message{} -// if err := msg.Unmarshal([]byte(`{"content":"hello world"}`)); err != nil { -// log.Fatal(err) -// } -// fmt.Println(msg.Content) -func (m *Message) Unmarshal(b []byte) error { - var byteMsg []byte - if err := json.Unmarshal(b, &byteMsg); err != nil { - return err - } - return nil -} - -// UnmarshalJSON converts the JSON (in bytes) to a message -// Because the *discordgo.Message struct is embedded in the Message struct and also has an UnmarshalJSON method, -// go will call the UnmarshalJSON method of the *discordgo.Message struct when the Message struct is marshaled -// unless we override it with our own UnmarshalJSON method in the Message struct, which we do -// Example: -// msg := &model.Message{} -// if err := msg.UnmarshalJSON([]byte(`{"content":"hello world"}`)); err != nil { -// log.Fatal(err) -// } -func (m *Message) UnmarshalJSON(b []byte) error { - msg := make(map[string]json.RawMessage) - - if err := json.Unmarshal(b, &msg); err != nil { - return err - } - - if err := json.Unmarshal(msg["message"], &m.Message); err != nil { - return err - } - - if err := json.Unmarshal(msg["metadata"], &m.Metadata); err != nil { - return err - } - - return nil -} - -// MarshalJSON converts the message to JSON -// Because the *discordgo.Message struct is embedded in the Message struct and also has a MarshalJSON method, -// go will call the MarshalJSON method of the *discordgo.Message struct when the Message struct is marshaled -// unless we override it with our own MarshalJSON method in the Message struct, which we do -// Example: -// msg := &model.Message{ -// Message: &discordgo.Message{ -// Content: "hello world", -// }, -// } -// b, err := msg.MarshalJSON() -// if err != nil { -// log.Fatal(err) -// } -// fmt.Println(string(b)) -func (m *Message) MarshalJSON() ([]byte, error) { - msg := make(map[string]interface{}) - - msg["message"] = m.Message - msg["metadata"] = m.Metadata - - return json.Marshal(msg) -} diff --git a/model/message_test.go b/model/message_test.go deleted file mode 100644 index 9dbc9c8..0000000 --- a/model/message_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package model - -import ( - "fmt" - "strings" - "testing" - - "github.com/bwmarrin/discordgo" - "github.com/r3labs/diff" - uuid "github.com/satori/go.uuid" -) - -// Values used in tests -const ( - TestChannelID = "test-channel-id" - TestInboundMetadataUUID = "00000000-0000-0000-0000-000000000000" - TestOutboundMetadataUUID = "11111111-1111-1111-1111-111111111111" - TestInboundDiscordMessageID = "test-inbound-discord-message-id" - TestOutboundDiscordMessageID = "test-outbound-discord-message-id" - TestInboundMessageBody = "test-inbound-message-body" - TestOutboundMessageBody = "test-outbound-message-body" - TestUserID = "test-user-id" - TestUserName = "test-user-name" - TestUserDiscriminator = "0000" - TestMetdataSource = "test-source" - TestMetdataDest = "test-dest" - TestAppName = "test-app" - TestGuildID = "test-guild-id" -) - -func TestMessage_UnmarshalJSON(t *testing.T) { - /* - This test case checks that the UnmarshalJSON method returns the correct Message struct - The function is intended to unmarshal a JSON string into a Message struct - This means the Message struct should have the same values as the JSON string - - Because the *discordgo.Message struct is embedded in the Message struct and also has a MarshalJSON method, - go will call the MarshalJSON method of the *discordgo.Message struct when the Message struct is marshaled - unless we override it with a custom MarshalJSON method in the Message struct, which we do - - */ - - tests := []struct { - name string - messageJSON []byte - want *Message - testCase *Message - wantErr bool - }{ - // Super basic test case - { - name: "hello world", - messageJSON: []byte(`{ - "metadata": { - "source": "` + TestMetdataSource + `", - "dest": "", - "id": "00000000-0000-0000-0000-000000000000" - }, - "message": { - "content": "` + TestInboundMessageBody + `", - "channel_id": "` + TestChannelID + `", - "author": { - "id": "` + TestUserID + `", - "username": "` + TestUserName + `", - "discriminator": "0000" - } - } - }`), - testCase: &Message{ - Message: &discordgo.Message{}, - Metadata: Metadata{}, - }, - want: &Message{ - Message: &discordgo.Message{ - Content: TestInboundMessageBody, - ChannelID: TestChannelID, - Author: &discordgo.User{ - ID: TestUserID, - Username: TestUserName, - Discriminator: TestUserDiscriminator, - }, - }, - Metadata: Metadata{ - Source: TestMetdataSource, - Dest: "", - ID: uuid.FromStringOrNil(TestOutboundMetadataUUID), - }, - }, - wantErr: false, - }, - // Full message test case - { - name: "Full message body", - messageJSON: []byte(` - { - "metadata": {}, - "message": { - "id": "` + TestInboundDiscordMessageID + `", - "channel_id": "` + TestChannelID + `", - "guild_id": "` + TestGuildID + `", - "content": "` + TestInboundMessageBody + `", - "edited_timestamp": null, - "mention_roles": [], - "tts": false, - "mention_everyone": false, - "author": { - "id": "` + TestUserID + `", - "email": "", - "username": "` + TestUserName + `", - "avatar": "", - "locale": "", - "discriminator": "` + TestUserDiscriminator + `", - "token": "", - "verified": false, - "mfa_enabled": false, - "banner": "", - "accent_color": 0, - "bot": false, - "public_flags": 0, - "premium_type": 0, - "system": false, - "flags": 0 - }, - "attachments": [], - "embeds": [], - "mentions": [], - "reactions": null, - "pinned": false, - "type": 0, - "webhook_id": "", - "member": { - "guild_id": "", - "nick": "", - "deaf": false, - "mute": false, - "avatar": "", - "user": null, - "roles": [], - "premium_since": null, - "pending": false, - "permissions": "0", - "communication_disabled_until": null - }, - "mention_channels": null, - "activity": null, - "application": null, - "message_reference": null, - "referenced_message": null, - "interaction": null, - "flags": 0, - "sticker_items": null - } - } - - `), - testCase: &Message{ - Message: &discordgo.Message{}, - Metadata: Metadata{}, - }, - want: &Message{ - Message: &discordgo.Message{ - ID: TestInboundDiscordMessageID, - ChannelID: TestChannelID, - GuildID: TestGuildID, - Content: TestInboundMessageBody, - MentionRoles: []string{}, - MentionEveryone: false, - Author: &discordgo.User{ - ID: TestUserID, - Email: "", - Avatar: "", - Locale: "", - Discriminator: TestUserDiscriminator, - Token: "", - Verified: false, - Banner: "", - AccentColor: 0, - Bot: false, - PublicFlags: 0, - PremiumType: 0, - System: false, - Flags: 0, - Username: TestUserName, - }, - Reactions: nil, - Pinned: false, - Type: 0, - WebhookID: "", - Member: &discordgo.Member{ - GuildID: "", - Nick: "", - Deaf: false, - Mute: false, - Avatar: "", - User: nil, - Roles: []string{}, - Pending: false, - }, - MentionChannels: nil, - Activity: nil, - Application: nil, - MessageReference: nil, - ReferencedMessage: nil, - Interaction: nil, - Flags: 0, - StickerItems: nil, - }, - Metadata: Metadata{}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.testCase.UnmarshalJSON(tt.messageJSON) - if (err != nil) != tt.wantErr { - fmt.Println(string(tt.messageJSON)) // Print the JSON for debugging - - t.Errorf("Message.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - return - } - - // Compare content of messages - if diff.Changed(tt.testCase.Message, tt.want.Message) { - t.Errorf("Message.UnmarshalJSON() Message does not match") - - d, err := diff.Diff(tt.testCase.Message, tt.want.Message) - if err != nil { - t.Errorf("Message.UnmarshalJSON() Diff error = %v", err) - } - - for _, c := range d { - t.Errorf("Compare this snippet from %s:\nWanted:\t%v\nGot:\t%v\n", strings.Join(c.Path, "."), c.From, c.To) - } - } - - // Compare metadata - if diff.Changed(tt.testCase.Metadata, tt.want.Metadata) { - t.Errorf("Message.UnmarshalJSON() Metadata does not match") - - d, err := diff.Diff(tt.testCase.Metadata, tt.want.Metadata) - if err != nil { - t.Errorf("Message.UnmarshalJSON() Diff error = %v", err) - } - - for _, c := range d { - t.Errorf("Compare this snippet from %s:\nWanted:\t%v\nGot:\t%v\n", strings.Join(c.Path, "."), c.From, c.To) - } - } - }) - } -} diff --git a/model/replies.go b/model/replies.go deleted file mode 100644 index b4602d8..0000000 --- a/model/replies.go +++ /dev/null @@ -1,142 +0,0 @@ -package model - -import ( - "encoding/json" - - "github.com/bwmarrin/discordgo" - uuid "github.com/satori/go.uuid" -) - -// MessageSend is the struct that is used to pass messages from the Redis pubsub to the Discord Gateway (outbound messages) -// Because the discordgo.Session.ChannelMessageSend() method only accepts channel ID and content as a string, our struct limits iteslef to those two fields as well. -// Future work may expand this to include more fields or expand metadata to include more information that can be used to forumlate more complex responses. -type MessageSend struct { - ChannelID string `json:"channel_id"` // ChannelID is the ID of the discord channel to send the message to - Content string `json:"content"` // Content is the text body of the message to send - Metadata Metadata `json:"metadata"` // Metadata is the metadata that is used to track the message - PreviousMessage *discordgo.Message `json:"previous_message"` // PreviousMessage is the message that triggered this message - ShouldReply bool `json:"should_reply"` // ShouldReply is a flag that indicates if the message should reply to the user that sent the previous message - ShouldMention bool `json:"should_mention"` // ShouldMention is a flag that indicates if the message should mention the user that sent the previous message -} - -// Deprecated in favor of newer methods that consume the entire model.Message struct -// MarshalReply converts the message to JSON and adds the metadata from the original message -// MarshalReply sends a response to the originating channel or direct message but does not do a "discord reply" -func (m *Message) MarshalReply(meta Metadata, dest string, s string) ([]byte, error) { - reply := &MessageSend{ - Content: s, - ChannelID: dest, - Metadata: meta, - } - return json.Marshal(reply) -} - -// RespondToChannelOrThread generates a MessageSend struct that can be used to respond to a channel or thread -// It optionally allows the message to reply or mention the user that sent the original message -// -// Typically when constructing replies you need access to the discordgo.Session but -// applications that use this library may not have access to that object, so it actually gets handled in the gateway -// this constraint forces the MessageSend struct to be a little bigger than I would like it to be but it's necessary -// for now to have the correct context to respond to messages -func (m *Message) RespondToChannelOrThread(sourceApp, content string, shouldReply, shouldMention bool) *MessageSend { - meta := Metadata{ - Source: sourceApp, - Dest: m.Metadata.Source, - ID: uuid.NewV4(), - } - - return &MessageSend{ - ChannelID: m.ChannelID, - Content: content, - Metadata: meta, - PreviousMessage: m.Message, - ShouldReply: shouldReply, - ShouldMention: shouldMention, - } -} - -// Unmarshal converts the JSON (in bytes) to a message -// This method is deprecated in favor of the UnmarshalJSON method and will be removed in a future release -// Correct behavior from this method is not guaranteed -// Example: -// msg := &model.MessageSend{} -// if err := msg.Unmarshal([]byte(`{"content":"hello world"}`)); err != nil { -// log.Fatal(err) -// } -// fmt.Println(msg.Content) -func (m *MessageSend) Unmarshal(b []byte) error { - if err := json.Unmarshal(b, m); err != nil { - return err - } - return nil -} - -// UnmarshalJSON converts the JSON (in bytes) to a message -// This method is preferred over the Unmarshal method and will be the only method in a future release -// Example: -// msg := &model.MessageSend{} -// if err := msg.UnmarshalJSON([]byte(`{"content":"hello world"}`)); err != nil { -// log.Fatal(err) -// } -// fmt.Println(msg.Content) -func (m *MessageSend) UnmarshalJSON(b []byte) error { - msg := make(map[string]json.RawMessage) - - if err := json.Unmarshal(b, &msg); err != nil { - return err - } - - if err := json.Unmarshal(msg["content"], &m.Content); err != nil { - return err - } - - if err := json.Unmarshal(msg["channel_id"], &m.ChannelID); err != nil { - return err - } - - if err := json.Unmarshal(msg["metadata"], &m.Metadata); err != nil { - return err - } - - if err := json.Unmarshal(msg["previous_message"], &m.PreviousMessage); err != nil { - return err - } - - if err := json.Unmarshal(msg["should_reply"], &m.ShouldReply); err != nil { - return err - } - - if err := json.Unmarshal(msg["should_mention"], &m.ShouldMention); err != nil { - return err - } - - return nil -} - -// MarshalJSON converts the message to JSON -// This method is preferred over the Marshal method and will be the only method in a future release -// Example: -// msg := &model.MessageSend{ -// Content: "hello world", -// Metadata: model.Metadata{ -// Source: "test", -// Dest: "discord", -// }, -// } -// b, err := msg.MarshalJSON() -// if err != nil { -// log.Fatal(err) -// } -// fmt.Println(string(b)) -func (m *MessageSend) MarshalJSON() ([]byte, error) { - msg := make(map[string]interface{}) - - msg["content"] = m.Content - msg["channel_id"] = m.ChannelID - msg["metadata"] = m.Metadata - msg["previous_message"] = m.PreviousMessage - msg["should_reply"] = m.ShouldReply - msg["should_mention"] = m.ShouldMention - - return json.Marshal(msg) -} diff --git a/model/replies_test.go b/model/replies_test.go deleted file mode 100644 index d1db967..0000000 --- a/model/replies_test.go +++ /dev/null @@ -1,288 +0,0 @@ -package model - -import ( - "reflect" - "strings" - "testing" - - "github.com/bwmarrin/discordgo" - "github.com/r3labs/diff/v3" - uuid "github.com/satori/go.uuid" -) - -func TestMessage_RespondToChannelOrThread(t *testing.T) { - // Setup test cases and expected results through this struct - // The test cases are the values of the Message struct that is passed to the RespondToChannelOrThread method - // The expected results are the values that the method should return once it has been - // marshaled into JSON and unmarshaled back into a MessageSend struct - tests := []struct { - name string // name of the test - message *Message // Original message to respond to - want *MessageSend // Expected response - sourceApp string // ID of the app sending the response - content string // Text to send in the response - shouldReply bool // Whether the response should be a reply to the original message - shouldMention bool // Whether the response should mention the original message author - wantErr bool // Whether or not the test should fail - }{ - { - name: "Basic, no replies", - message: &Message{ - Message: &discordgo.Message{ - Content: TestInboundMessageBody, - ChannelID: TestChannelID, - }, - Metadata: Metadata{ - Source: TestMetdataSource, // Inbound messages should always have a source or else no app will know where to send responses - Dest: TestMetdataDest, // Inbound messages typically will not have a destination - ID: uuid.FromStringOrNil(TestInboundMetadataUUID), // Usually this is set by the app, but we can set it here for testing - }, - }, - want: &MessageSend{ - Content: TestOutboundMessageBody, - ChannelID: TestChannelID, - Metadata: Metadata{ - Source: TestAppName, // Should be the app name - Dest: TestMetdataSource, // Outbound messages should always have a destination or else no app will know to process them - ID: uuid.FromStringOrNil(TestOutboundMetadataUUID), - }, - PreviousMessage: &discordgo.Message{ - Content: TestInboundMessageBody, - ChannelID: TestChannelID, - }, - }, - sourceApp: TestAppName, - content: TestOutboundMessageBody, - wantErr: false, - shouldReply: false, - shouldMention: false, - }, - { - name: "Reply with no mention", - message: &Message{ - Message: &discordgo.Message{ - ID: TestInboundDiscordMessageID, // This is the ID of the message we will be replying to. - ChannelID: TestChannelID, // This is the ID of the channel the message originated from (or the thread if it was a thread). - Content: TestInboundMessageBody, - GuildID: TestGuildID, - }, - Metadata: Metadata{ - Source: TestMetdataSource, // Inbound messages should always have a source or else no app will know where to send responses - Dest: TestMetdataDest, // Inbound messages typically will not have a destination but we can set it here for testing - ID: uuid.FromStringOrNil(TestInboundMetadataUUID), // Usually this is set by the app, but we can set it here for testing - }, - }, - want: &MessageSend{ - Content: TestOutboundMessageBody, // This is the text we want to send in the response - ChannelID: TestChannelID, // ChannelID should be the same as the original message. Comes from Message.ChannelID. - Metadata: Metadata{ - Source: TestAppName, // Source should be the app sending the response - Dest: TestMetdataSource, // Dest should be the source from the original message. Comes from Message.Metadata.Source. - ID: uuid.FromStringOrNil(TestOutboundMetadataUUID), // ID should be a new UUID v4 - }, - PreviousMessage: &discordgo.Message{ - ID: TestInboundDiscordMessageID, // This is the ID of the message we will be replying to. - ChannelID: TestChannelID, // This is the ID of the channel the message originated from (or the thread if it was a thread). - Content: TestInboundMessageBody, - GuildID: TestGuildID, - }, - ShouldReply: true, // This should be true because we are replying to a message - ShouldMention: false, // This should be false because we are not mentioning the original message author - }, - sourceApp: TestAppName, - content: TestOutboundMessageBody, - wantErr: false, - shouldReply: true, // This should be true because we are replying to a message - shouldMention: false, // This should be false because we are not mentioning the original message author - }, - { - name: "Reply with mention", - message: &Message{ - Message: &discordgo.Message{ - ID: TestInboundDiscordMessageID, - ChannelID: TestChannelID, - Content: TestInboundMessageBody, - GuildID: TestGuildID, - }, - Metadata: Metadata{ - Source: TestMetdataSource, - Dest: TestMetdataDest, - ID: uuid.FromStringOrNil(TestInboundMetadataUUID), - }, - }, - want: &MessageSend{ - Content: TestOutboundMessageBody, - ChannelID: TestChannelID, // ChannelID should be the same as the original message - Metadata: Metadata{ - Source: TestAppName, // Source should be the app sending the response - Dest: TestMetdataSource, // Dest should be the source from the original message - ID: uuid.FromStringOrNil(TestOutboundMetadataUUID), // ID should be a new UUID - }, - PreviousMessage: &discordgo.Message{ - ID: TestInboundDiscordMessageID, - ChannelID: TestChannelID, - Content: TestInboundMessageBody, - GuildID: TestGuildID, - }, - ShouldReply: true, // This should be true because we are replying to a message - ShouldMention: true, // This should be true because we are mentioning the original message author - }, - sourceApp: TestAppName, - content: TestOutboundMessageBody, - wantErr: false, - shouldReply: true, - shouldMention: true, - }, - } - - // Iterate through the test cases - for _, tt := range tests { - - t.Run(tt.name, func(t *testing.T) { - - // Create a new MessageSend struct - got := tt.message.RespondToChannelOrThread(tt.sourceApp, tt.content, tt.shouldReply, tt.shouldMention) - - // Setup a filter to ignore the ID field - filter := diff.Filter( - func(path []string, parent reflect.Type, field reflect.StructField) bool { - return field.Name != "ID" - }) - - changelog, err := diff.Diff(tt.want, got, filter) - if err != nil { - t.Errorf("Message_RespondToChannelOrThread() error = %v", err) - return - } - - // If the changelog is not empty, the test has failed - if len(changelog) != 0 { - // Print the changelog to the console - for _, c := range changelog { - t.Errorf("Message.RespondToChannelOrThread() - %s\nCompare this snippet from %s:\nWanted:\t%v\nGot:\t%v\n", tt.name, strings.Join(c.Path, "."), c.From, c.To) - } - } - }) - } -} - -func TestMessageSend_UnmarshalJSON(t *testing.T) { - type fields struct { - ChannelID string - Content string - Metadata Metadata - ShouldReply bool - ShouldMention bool - } - type args struct { - b []byte - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "Valid JSON", - fields: fields{ - ChannelID: TestChannelID, - Content: TestOutboundMessageBody, - Metadata: Metadata{ - Source: TestAppName, - Dest: TestMetdataSource, // Outbound messages should always have a destination or else no app will know to process them - ID: uuid.FromStringOrNil(TestOutboundMetadataUUID), - }, - }, - args: args{ - b: []byte(`{ - "channel_id": "` + TestChannelID + `", - "content": "` + TestOutboundMessageBody + `", - "metadata": { - "source": "` + TestAppName + `", - "dest": "` + TestMetdataSource + `", - "id": "` + TestOutboundMetadataUUID + `" - }, - "should_reply": false, - "should_mention": false, - "previous_message": null - }`), - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := &MessageSend{ - ChannelID: tt.fields.ChannelID, - Content: tt.fields.Content, - Metadata: tt.fields.Metadata, - ShouldReply: tt.fields.ShouldReply, - ShouldMention: tt.fields.ShouldMention, - PreviousMessage: &discordgo.Message{}, - } - if err := m.UnmarshalJSON(tt.args.b); (err != nil) != tt.wantErr { - t.Errorf("MessageSend.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestMessageSend_MarshalJSON(t *testing.T) { - type fields struct { - ChannelID string - Content string - Metadata Metadata - PreviousMessage *discordgo.Message - ShouldReply bool - ShouldMention bool - } - tests := []struct { - name string - fields fields - want []byte - wantErr bool - }{ - { - name: "Valid JSON", - fields: fields{ - ChannelID: TestChannelID, - Content: TestOutboundMessageBody, - Metadata: Metadata{ - Source: TestAppName, - Dest: TestMetdataSource, // Outbound messages should always have a destination or else no app will know to process them - ID: uuid.FromStringOrNil(TestOutboundMetadataUUID), - }, - ShouldReply: false, - ShouldMention: false, - }, - want: []byte(`{"channel_id":"` + TestChannelID + `","content":"` + TestOutboundMessageBody + `","metadata":{"source":"` + TestAppName + `","dest":"` + TestMetdataSource + `","id":"` + TestOutboundMetadataUUID + `"},"previous_message":null,"should_mention":false,"should_reply":false}`), - - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - m := &MessageSend{ - ChannelID: tt.fields.ChannelID, - Content: tt.fields.Content, - Metadata: tt.fields.Metadata, - } - got, err := m.MarshalJSON() - if (err != nil) != tt.wantErr { - t.Errorf("MessageSend.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - return - } - - // Compare the content of the messages - d, err := diff.Diff(string(got), string(tt.want)) - if err != nil { - t.Errorf("MessageSend.MarshalJSON() Diff error = %v", err) - } - - for _, c := range d { - t.Errorf("MessageSend.MarshalJSON() - %s\nCompare this snippet from %s:\nGot:\t%v\nWanted:\t%v\n", tt.name, strings.Join(c.Path, "."), c.From, c.To) - } - }) - } -}