Skip to content

Commit

Permalink
Merge pull request #190 from robinje/motd
Browse files Browse the repository at this point in the history
Add Message of the Day
  • Loading branch information
robinje authored Sep 18, 2024
2 parents 3158724 + 897a05d commit d7c2f3d
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 27 deletions.
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

0 comments on commit d7c2f3d

Please sign in to comment.