Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Message of the Day #190

Merged
merged 13 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ 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.
- [ ] Validate graph of loaded rooms and exits.
- [ ] Add look at item command.
- [ ] Improve the say commands.
- [ ] Improve the input filters
- [ ] Create administrative interface.



Expand Down
19 changes: 19 additions & 0 deletions cloudformation/dynamo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
78 changes: 78 additions & 0 deletions core/motd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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"
"github.com/google/uuid"
)

func (k *KeyPair) GetAllMOTDs() ([]*MOTD, error) {
input := &dynamodb.ScanInput{
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)
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
}

// 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
}
}
}
10 changes: 10 additions & 0 deletions core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type Server struct {
ItemPrototypes map[uint64]*Item
Context context.Context
Mutex sync.Mutex
ActiveMotDs []*MOTD
}

type Player struct {
Expand All @@ -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 {
Expand Down Expand Up @@ -215,3 +218,10 @@ type CloudWatchHandler struct {
type MultiHandler struct {
handlers []slog.Handler
}

type MOTD struct {
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"`
}
42 changes: 42 additions & 0 deletions database/motd.py
Original file line number Diff line number Diff line change
@@ -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()
67 changes: 42 additions & 25 deletions scripts/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
# Configuration file path
CONFIG_PATH = "../ssh_server/config.yml"


def prompt_for_parameters(template_name):
if template_name == "cognito":
return {
"UserPoolName": input("Enter the Name of the user pool [default: mud-user-pool]: ") or "mud-user-pool",
"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: "),
}
Expand All @@ -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:
Expand All @@ -70,24 +76,28 @@ 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)
return True
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:
Expand All @@ -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)
Expand All @@ -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")

Expand All @@ -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,
Expand All @@ -176,5 +192,6 @@ def main():
}
update_configuration_file(config_updates)


if __name__ == "__main__":
main()
main()
12 changes: 11 additions & 1 deletion ssh_server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,17 @@ 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)
} else {
server.ActiveMotDs = activeMOTDs
core.Logger.Info("Loaded active MOTDs", "count", len(activeMOTDs))
}

return server, nil

}

func loadConfiguration(configFile string) (core.Configuration, error) {
Expand Down Expand Up @@ -263,7 +273,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)
Expand Down
Loading