From 8c75b20a9316395da3b9e2588f60855ace5e0d6b Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Sun, 8 Sep 2024 05:21:51 -0400 Subject: [PATCH 01/10] WIP --- cloudformation/dynamo.yml | 19 +++++++++++++++++++ core/types.go | 7 +++++++ 2 files changed, 26 insertions(+) diff --git a/cloudformation/dynamo.yml b/cloudformation/dynamo.yml index 5775d54..174854f 100644 --- a/cloudformation/dynamo.yml +++ b/cloudformation/dynamo.yml @@ -104,6 +104,20 @@ Resources: ReadCapacityUnits: 2 WriteCapacityUnits: 2 + MOTDTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: motd + AttributeDefinitions: + - AttributeName: motdID + AttributeType: S + KeySchema: + - AttributeName: motdID + KeyType: HASH + ProvisionedThroughput: + ReadCapacityUnits: 2 + WriteCapacityUnits: 2 + MUDDynamoDBPolicy: Type: AWS::IAM::ManagedPolicy Properties: @@ -129,6 +143,7 @@ Resources: - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/items' - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/prototypes' - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/archetypes' + - !Sub 'arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/motd' Outputs: PlayersTableArn: @@ -159,6 +174,10 @@ Outputs: Description: "ARN of the Archetypes table" Value: !GetAtt ArchetypesTable.Arn + MOTDTableArn: + Description: "ARN of the MotD table" + Value: !GetAtt MOTDTable.Arn + MUDDynamoDBPolicyArn: Description: "ARN of the MUD DynamoDB Read/Write Policy" Value: !Ref MUDDynamoDBPolicy \ No newline at end of file diff --git a/core/types.go b/core/types.go index fd487db..291d826 100644 --- a/core/types.go +++ b/core/types.go @@ -143,6 +143,7 @@ type CharacterData struct { Health float64 `json:"health" dynamodbav:"Health"` RoomID int64 `json:"roomID" dynamodbav:"RoomID"` Inventory map[string]string `json:"inventory" dynamodbav:"Inventory"` + MotD []string `json:"motd" dynamodbav:"MotD"` } type Archetype struct { @@ -215,3 +216,9 @@ type CloudWatchHandler struct { type MultiHandler struct { handlers []slog.Handler } + +type MOTD struct { + ID uuid.UUID `json:"id" dynamodbav:"ID"` + Message string `json:"message" dynamodbav:"Message"` + CreatedAt time.Time `json:"createdAt" dynamodbav:"CreatedAt"` +} From 8602e7413653391a668dd6f4fb6a2dfe77b8fc3d Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Mon, 9 Sep 2024 02:18:51 -0400 Subject: [PATCH 02/10] WIP --- core/types.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/types.go b/core/types.go index 291d826..aaf9fcb 100644 --- a/core/types.go +++ b/core/types.go @@ -74,6 +74,7 @@ type Server struct { ItemPrototypes map[uint64]*Item Context context.Context Mutex sync.Mutex + ActiveMotDs []*MOTD } type Player struct { @@ -94,11 +95,13 @@ type Player struct { LoginTime time.Time PasswordHash string Mutex sync.Mutex + SeenMotDs map[uuid.UUID]bool } type PlayerData struct { Name string `json:"name" dynamodbav:"Name"` CharacterList map[string]string `json:"characterList" dynamodbav:"CharacterList"` + SeenMotDs map[string]bool `json:"seenMotDs" dynamodbav:"SeenMotDs"` } type Room struct { @@ -143,7 +146,6 @@ type CharacterData struct { Health float64 `json:"health" dynamodbav:"Health"` RoomID int64 `json:"roomID" dynamodbav:"RoomID"` Inventory map[string]string `json:"inventory" dynamodbav:"Inventory"` - MotD []string `json:"motd" dynamodbav:"MotD"` } type Archetype struct { @@ -218,7 +220,8 @@ type MultiHandler struct { } type MOTD struct { - ID uuid.UUID `json:"id" dynamodbav:"ID"` + ID uuid.UUID `json:"motdID" dynamodbav:"motdID"` + Active bool `json:"active" dynamodbav:"Active"` Message string `json:"message" dynamodbav:"Message"` CreatedAt time.Time `json:"createdAt" dynamodbav:"CreatedAt"` } From 68293411ef1d987eb093cb510085f4cd548020f5 Mon Sep 17 00:00:00 2001 From: "Jason E. Robinson" Date: Tue, 10 Sep 2024 11:04:46 -0400 Subject: [PATCH 03/10] Update Readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c70f847..59ed590 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ The current implementation includes an SSH server for secure authentication and - [ ] Add look at item command. - [ ] Improve the say commands. - [ ] Improve the input filters +- [ ] Create administrative interface. From ba2aca0c2d620d95665d76c143315d2ce2e35de0 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Sun, 15 Sep 2024 00:49:14 -0400 Subject: [PATCH 04/10] WIP --- core/motd.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 core/motd.go diff --git a/core/motd.go b/core/motd.go new file mode 100644 index 0000000..6b57cb9 --- /dev/null +++ b/core/motd.go @@ -0,0 +1,28 @@ +package core + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" +) + +func (k *KeyPair) GetAllMOTDs() ([]*MOTD, error) { + input := &dynamodb.ScanInput{ + TableName: aws.String("motd"), + } + + result, err := k.db.Scan(input) + if err != nil { + return nil, fmt.Errorf("error scanning MOTDs: %w", err) + } + + var motds []*MOTD + err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &motds) + if err != nil { + return nil, fmt.Errorf("error unmarshalling MOTDs: %w", err) + } + + return motds, nil +} From 3922045a5af608bbe2066f284705c8f9dd756a90 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 17 Sep 2024 00:21:38 -0400 Subject: [PATCH 05/10] Revise MOTD Loader to filter for active only --- core/motd.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/motd.go b/core/motd.go index 6b57cb9..c4f7086 100644 --- a/core/motd.go +++ b/core/motd.go @@ -10,7 +10,13 @@ import ( func (k *KeyPair) GetAllMOTDs() ([]*MOTD, error) { input := &dynamodb.ScanInput{ - TableName: aws.String("motd"), + TableName: aws.String("motd"), + FilterExpression: aws.String("active = :active"), + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":active": { + BOOL: aws.Bool(true), + }, + }, } result, err := k.db.Scan(input) From 15e0ec21ebc2b2b82c782dafc602ddfac8430b08 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 17 Sep 2024 00:46:28 -0400 Subject: [PATCH 06/10] Add display MOTD code. --- core/motd.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ ssh_server/server.go | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/motd.go b/core/motd.go index c4f7086..22b4f66 100644 --- a/core/motd.go +++ b/core/motd.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/google/uuid" ) func (k *KeyPair) GetAllMOTDs() ([]*MOTD, error) { @@ -32,3 +33,46 @@ func (k *KeyPair) GetAllMOTDs() ([]*MOTD, error) { return motds, nil } + +// TODO: Rework the 'server.ActiveMotDs' into a map. +func DisplayUnseenMOTDs(server *Server, player *Player) { + if server == nil || player == nil { + Logger.Error("Invalid server or player object") + return + } + + Logger.Info("Displaying MOTDs for player", "playerName", player.Name) + + defaultMOTDID, _ := uuid.Parse("00000000-0000-0000-0000-000000000000") + welcomeDisplayed := false + + // First, look for and display the welcome message + for _, motd := range server.ActiveMotDs { + if motd != nil && motd.ID == defaultMOTDID { + player.ToPlayer <- fmt.Sprintf("\n\r%s\n\r", motd.Message) + welcomeDisplayed = true + break + } + } + + // If no welcome message was found, display a generic one + if !welcomeDisplayed { + player.ToPlayer <- "\n\rWelcome to the game!\n\r" + } + + // Then display other unseen MOTDs + for _, motd := range server.ActiveMotDs { + if motd == nil || motd.ID == defaultMOTDID { + continue + } + + // Check if the player has already seen this MOTD + if !player.SeenMotDs[motd.ID] { + // Display the MOTD to the player + player.ToPlayer <- fmt.Sprintf("\n\r%s\n\r", motd.Message) + + // Mark the MOTD as seen + player.SeenMotDs[motd.ID] = true + } + } +} diff --git a/ssh_server/server.go b/ssh_server/server.go index 328780b..356d229 100644 --- a/ssh_server/server.go +++ b/ssh_server/server.go @@ -263,7 +263,7 @@ func handleChannels(server *core.Server, sshConn *ssh.ServerConn, channels <-cha core.Logger.Info("Player connected", "player_name", p.Name) // Send welcome message - p.ToPlayer <- fmt.Sprintf("Welcome to the game, %s!\n\r", p.Name) + core.DisplayUnseenMOTDs(server, p) // Character Selection Dialog character, _ := core.SelectCharacter(p, server) From 8320fa18a7570f679f4f1b2016ac4b8f76e4e37c Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 17 Sep 2024 00:50:09 -0400 Subject: [PATCH 07/10] Autoload the MOTD --- ssh_server/server.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ssh_server/server.go b/ssh_server/server.go index 356d229..bf4daaa 100644 --- a/ssh_server/server.go +++ b/ssh_server/server.go @@ -72,7 +72,18 @@ func NewServer(config core.Configuration) (*core.Server, error) { return nil, fmt.Errorf("failed to load rooms: %v", err) } + // Load active MOTDs + activeMOTDs, err := server.Database.GetAllMOTDs() + if err != nil { + core.Logger.Error("Failed to load active MOTDs", "error", err) + // Consider whether to return an error here or just continue with an empty MOTD list + } else { + server.ActiveMotDs = activeMOTDs + core.Logger.Info("Loaded active MOTDs", "count", len(activeMOTDs)) + } + return server, nil + } func loadConfiguration(configFile string) (core.Configuration, error) { From 2338a42490ce30f09a0022a23bfb677650f8e8ae Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 17 Sep 2024 00:56:31 -0400 Subject: [PATCH 08/10] Add MOTD Script --- database/motd.py | 42 +++++++++++++++++++++++++++++ scripts/deploy.py | 67 +++++++++++++++++++++++++++++------------------ 2 files changed, 84 insertions(+), 25 deletions(-) create mode 100644 database/motd.py diff --git a/database/motd.py b/database/motd.py new file mode 100644 index 0000000..b36715c --- /dev/null +++ b/database/motd.py @@ -0,0 +1,42 @@ +import argparse +import uuid +from datetime import datetime + +import boto3 + + +def add_or_update_motd(message, active=True, is_welcome=False): + dynamodb = boto3.resource("dynamodb") + table = dynamodb.Table("motd") + + motd_id = "00000000-0000-0000-0000-000000000000" if is_welcome else str(uuid.uuid4()) + + try: + response = table.update_item( + Key={"motdID": motd_id}, + UpdateExpression="SET #msg = :message, active = :active, createdAt = :created", + ExpressionAttributeNames={"#msg": "message"}, # 'message' is a reserved word in DynamoDB + ExpressionAttributeValues={":message": message, ":active": active, ":created": datetime.now().isoformat()}, + ReturnValues="UPDATED_NEW", + ) + print(f"MOTD {'updated' if is_welcome else 'added'} successfully.") + print(f"MOTD ID: {motd_id}") + return response + except Exception as e: + print(f"Error adding/updating MOTD: {str(e)}") + return None + + +def main(): + parser = argparse.ArgumentParser(description="Add or update a Message of the Day (MOTD)") + parser.add_argument("message", type=str, help="The MOTD message") + parser.add_argument("--welcome", action="store_true", help="Set this flag to add/update the welcome message") + parser.add_argument("--inactive", action="store_true", help="Set this flag to make the MOTD inactive") + + args = parser.parse_args() + + add_or_update_motd(args.message, not args.inactive, args.welcome) + + +if __name__ == "__main__": + main() diff --git a/scripts/deploy.py b/scripts/deploy.py index 0f99430..e21b4b4 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -17,6 +17,7 @@ # Configuration file path CONFIG_PATH = "../ssh_server/config.yml" + def prompt_for_parameters(template_name): if template_name == "cognito": return { @@ -24,7 +25,9 @@ def prompt_for_parameters(template_name): "AppClientName": input("Enter the Name of the app client [default: mud-app-client]: ") or "mud-app-client", "CallbackURL": input("Enter the URL of the callback for the app client [default: https://localhost:3000/callback]: ") or "https://localhost:3000/callback", - "SignOutURL": input("Enter the URL of the sign-out page for the app client [default: https://localhost:3000/sign-out]: ") + "SignOutURL": input( + "Enter the URL of the sign-out page for the app client [default: https://localhost:3000/sign-out]: " + ) or "https://localhost:3000/sign-out", "ReplyEmailAddress": input("Enter the email address to send from: "), } @@ -39,14 +42,17 @@ def prompt_for_parameters(template_name): return { "LogGroupName": input("Enter the name for the CloudWatch Log Group [default: /mud/game-logs]: ") or "/mud/game-logs", "RetentionInDays": input("Enter the number of days to retain logs [default: 30]: ") or "30", - "MetricNamespace": input("Enter the namespace for CloudWatch Metrics [default: MUD/Application]: ") or "MUD/Application", + "MetricNamespace": input("Enter the namespace for CloudWatch Metrics [default: MUD/Application]: ") + or "MUD/Application", } return {} + def load_template(template_path): with open(template_path, "r", encoding="utf-8") as file: return file.read() + def deploy_stack(client, stack_name, template_body, parameters): cf_parameters = [{"ParameterKey": k, "ParameterValue": v} for k, v in parameters.items()] try: @@ -70,6 +76,7 @@ def deploy_stack(client, stack_name, template_body, parameters): except ClientError as err: print(f"Error in stack operation: {err}") + def stack_exists(client, stack_name): try: client.describe_stacks(StackName=stack_name) @@ -77,17 +84,20 @@ def stack_exists(client, stack_name): except client.exceptions.ClientError: return False + def wait_for_stack_completion(client, stack_name): print(f"Waiting for stack {stack_name} to complete...") waiter = client.get_waiter("stack_create_complete") waiter.wait(StackName=stack_name) print("Stack operation completed.") + def get_stack_outputs(client, stack_name): stack = client.describe_stacks(StackName=stack_name) outputs = stack["Stacks"][0]["Outputs"] return {output["OutputKey"]: output["OutputValue"] for output in outputs} + def update_configuration_file(config_updates): try: with open(CONFIG_PATH, "r", encoding="utf-8") as file: @@ -109,26 +119,32 @@ def update_configuration_file(config_updates): config.setdefault("Server", {})["Port"] = config.get("Port", 9050) config.setdefault("Aws", {})["Region"] = config.get("Region", "us-east-1") config.setdefault("Cognito", {}).update(config_updates.get("Cognito", {})) - config.setdefault("Game", {}).update({ - "Balance": config.get("Balance", 0.25), - "AutoSave": config.get("AutoSave", 5), - "StartingHealth": config.get("StartingHealth", 10), - "StartingEssence": config.get("StartingEssence", 3), - }) - config.setdefault("Logging", {}).update({ - "ApplicationName": "mud", - "LogLevel": 20, - "LogGroup": config_updates.get("CloudWatch", {}).get("LogGroupName", "/mud"), - "LogStream": "application", - "MetricNamespace": config_updates.get("CloudWatch", {}).get("MetricNamespace", "MUD/Application"), - }) - config["Cognito"].update({ - "UserPoolId": config_updates.get("UserPoolId", ""), - "UserPoolClientSecret": config_updates.get("UserPoolClientSecret", ""), - "UserPoolClientId": config_updates.get("UserPoolClientId", ""), - "UserPoolDomain": config_updates.get("UserPoolDomain", ""), - "UserPoolArn": config_updates.get("UserPoolArn", ""), - }) + config.setdefault("Game", {}).update( + { + "Balance": config.get("Balance", 0.25), + "AutoSave": config.get("AutoSave", 5), + "StartingHealth": config.get("StartingHealth", 10), + "StartingEssence": config.get("StartingEssence", 3), + } + ) + config.setdefault("Logging", {}).update( + { + "ApplicationName": "mud", + "LogLevel": 20, + "LogGroup": config_updates.get("CloudWatch", {}).get("LogGroupName", "/mud"), + "LogStream": "application", + "MetricNamespace": config_updates.get("CloudWatch", {}).get("MetricNamespace", "MUD/Application"), + } + ) + config["Cognito"].update( + { + "UserPoolId": config_updates.get("UserPoolId", ""), + "UserPoolClientSecret": config_updates.get("UserPoolClientSecret", ""), + "UserPoolClientId": config_updates.get("UserPoolClientId", ""), + "UserPoolDomain": config_updates.get("UserPoolDomain", ""), + "UserPoolArn": config_updates.get("UserPoolArn", ""), + } + ) with open(CONFIG_PATH, "w", encoding="utf-8") as file: yaml.dump(config, file, default_flow_style=False) @@ -137,6 +153,7 @@ def update_configuration_file(config_updates): except Exception as err: print(f"An error occurred while updating configuration file: {err}") + def main(): cloudformation_client = boto3.client("cloudformation") @@ -160,13 +177,12 @@ def main(): deploy_stack(cloudformation_client, CODEBUILD_STACK_NAME, codebuild_template, codebuild_parameters) codebuild_outputs = get_stack_outputs(cloudformation_client, CODEBUILD_STACK_NAME) - # Deploy CloudWatch stack + # Deploy CloudWatch stack cloudwatch_parameters = prompt_for_parameters("cloudwatch") cloudwatch_template = load_template(CLOUDWATCH_TEMPLATE_PATH) deploy_stack(cloudformation_client, CLOUDWATCH_STACK_NAME, cloudwatch_template, cloudwatch_parameters) cloudwatch_outputs = get_stack_outputs(cloudformation_client, CLOUDWATCH_STACK_NAME) - # Update configuration file with outputs from all stacks config_updates = { "Cognito": cognito_outputs, @@ -176,5 +192,6 @@ def main(): } update_configuration_file(config_updates) + if __name__ == "__main__": - main() \ No newline at end of file + main() From 459e660c5b1236294eef683ac87ca0e1dd747dd0 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 17 Sep 2024 00:59:35 -0400 Subject: [PATCH 09/10] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59ed590..b70bb1e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ The current implementation includes an SSH server for secure authentication and - [x] Load item prototypes at start. - [x] Create function for creating items from prototypes. - [x] Ensure that a message is passed when a characters is added to the game. -- [ ] Add a Message of the Day (MOTD) command. +- [x] Add a Message of the Day (MOTD) command. - [ ] Add the ability to delete characters. - [ ] Add the ability to delete accounts. - [ ] Implement an obscenity filter. From c0f906d73934175ccd6881281a6fb6d5f4ee29c0 Mon Sep 17 00:00:00 2001 From: Jason Robinson Date: Tue, 17 Sep 2024 13:13:03 -0400 Subject: [PATCH 10/10] Update server.go --- ssh_server/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ssh_server/server.go b/ssh_server/server.go index bf4daaa..59cf4ee 100644 --- a/ssh_server/server.go +++ b/ssh_server/server.go @@ -76,7 +76,6 @@ func NewServer(config core.Configuration) (*core.Server, error) { activeMOTDs, err := server.Database.GetAllMOTDs() if err != nil { core.Logger.Error("Failed to load active MOTDs", "error", err) - // Consider whether to return an error here or just continue with an empty MOTD list } else { server.ActiveMotDs = activeMOTDs core.Logger.Info("Loaded active MOTDs", "count", len(activeMOTDs))