399 lines
12 KiB
Go
399 lines
12 KiB
Go
package chat
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"time"
|
|
|
|
"eq2emu/internal/database"
|
|
)
|
|
|
|
// Channel represents a chat channel with membership and message routing capabilities
|
|
type Channel struct {
|
|
ID int32 `json:"id"` // Channel ID
|
|
Name string `json:"name"` // Channel name
|
|
Password string `json:"-"` // Channel password (hidden from JSON)
|
|
ChannelType int `json:"type"` // Channel type (world/custom)
|
|
LevelRestriction int32 `json:"level_restriction"`
|
|
RaceRestriction int32 `json:"race_restriction"`
|
|
ClassRestriction int32 `json:"class_restriction"`
|
|
DiscordEnabled bool `json:"discord_enabled"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
|
|
members []int32 `json:"-"` // Character IDs (not persisted)
|
|
db *database.Database `json:"-"` // Database connection
|
|
isNew bool `json:"-"` // Whether this is a new channel
|
|
}
|
|
|
|
// New creates a new channel with the given database
|
|
func New(db *database.Database) *Channel {
|
|
return &Channel{
|
|
db: db,
|
|
isNew: true,
|
|
members: make([]int32, 0),
|
|
Created: time.Now(),
|
|
Updated: time.Now(),
|
|
}
|
|
}
|
|
|
|
// NewWithData creates a new channel with data
|
|
func NewWithData(id int32, name string, channelType int, db *database.Database) *Channel {
|
|
return &Channel{
|
|
ID: id,
|
|
Name: name,
|
|
ChannelType: channelType,
|
|
db: db,
|
|
isNew: true,
|
|
members: make([]int32, 0),
|
|
Created: time.Now(),
|
|
Updated: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Load loads a channel by ID from the database
|
|
func Load(db *database.Database, id int32) (*Channel, error) {
|
|
channel := &Channel{
|
|
db: db,
|
|
isNew: false,
|
|
members: make([]int32, 0),
|
|
}
|
|
|
|
query := `SELECT id, name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels WHERE id = ?`
|
|
row := db.QueryRow(query, id)
|
|
|
|
err := row.Scan(&channel.ID, &channel.Name, &channel.Password, &channel.ChannelType,
|
|
&channel.LevelRestriction, &channel.RaceRestriction, &channel.ClassRestriction,
|
|
&channel.DiscordEnabled, &channel.Created, &channel.Updated)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load channel %d: %w", id, err)
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// LoadByName loads a channel by name from the database
|
|
func LoadByName(db *database.Database, name string) (*Channel, error) {
|
|
channel := &Channel{
|
|
db: db,
|
|
isNew: false,
|
|
members: make([]int32, 0),
|
|
}
|
|
|
|
query := `SELECT id, name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels WHERE name = ?`
|
|
row := db.QueryRow(query, name)
|
|
|
|
err := row.Scan(&channel.ID, &channel.Name, &channel.Password, &channel.ChannelType,
|
|
&channel.LevelRestriction, &channel.RaceRestriction, &channel.ClassRestriction,
|
|
&channel.DiscordEnabled, &channel.Created, &channel.Updated)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load channel %s: %w", name, err)
|
|
}
|
|
|
|
return channel, nil
|
|
}
|
|
|
|
// GetID returns the channel ID (implements Identifiable interface)
|
|
func (c *Channel) GetID() int32 {
|
|
return c.ID
|
|
}
|
|
|
|
// SetName sets the channel name
|
|
func (c *Channel) SetName(name string) {
|
|
c.Name = name
|
|
c.Updated = time.Now()
|
|
}
|
|
|
|
// SetPassword sets the channel password
|
|
func (c *Channel) SetPassword(password string) {
|
|
c.Password = password
|
|
c.Updated = time.Now()
|
|
}
|
|
|
|
// SetType sets the channel type
|
|
func (c *Channel) SetType(channelType int) {
|
|
c.ChannelType = channelType
|
|
c.Updated = time.Now()
|
|
}
|
|
|
|
// SetLevelRestriction sets the minimum level required to join
|
|
func (c *Channel) SetLevelRestriction(level int32) {
|
|
c.LevelRestriction = level
|
|
c.Updated = time.Now()
|
|
}
|
|
|
|
// SetRacesAllowed sets the race bitmask for allowed races
|
|
func (c *Channel) SetRacesAllowed(races int32) {
|
|
c.RaceRestriction = races
|
|
c.Updated = time.Now()
|
|
}
|
|
|
|
// SetClassesAllowed sets the class bitmask for allowed classes
|
|
func (c *Channel) SetClassesAllowed(classes int32) {
|
|
c.ClassRestriction = classes
|
|
c.Updated = time.Now()
|
|
}
|
|
|
|
// GetName returns the channel name
|
|
func (c *Channel) GetName() string {
|
|
return c.Name
|
|
}
|
|
|
|
// GetType returns the channel type
|
|
func (c *Channel) GetType() int {
|
|
return c.ChannelType
|
|
}
|
|
|
|
// GetNumClients returns the number of clients in the channel
|
|
func (c *Channel) GetNumClients() int {
|
|
return len(c.members)
|
|
}
|
|
|
|
// HasPassword returns true if the channel has a password
|
|
func (c *Channel) HasPassword() bool {
|
|
return c.Password != ""
|
|
}
|
|
|
|
// PasswordMatches checks if the provided password matches the channel password
|
|
func (c *Channel) PasswordMatches(password string) bool {
|
|
return c.Password == password
|
|
}
|
|
|
|
// CanJoinChannelByLevel checks if a player's level meets the channel requirements
|
|
func (c *Channel) CanJoinChannelByLevel(level int32) bool {
|
|
return level >= c.LevelRestriction
|
|
}
|
|
|
|
// CanJoinChannelByRace checks if a player's race is allowed in the channel
|
|
func (c *Channel) CanJoinChannelByRace(raceID int32) bool {
|
|
return c.RaceRestriction == NoRaceRestriction || (c.RaceRestriction&(1<<raceID)) != 0
|
|
}
|
|
|
|
// CanJoinChannelByClass checks if a player's class is allowed in the channel
|
|
func (c *Channel) CanJoinChannelByClass(classID int32) bool {
|
|
return c.ClassRestriction == NoClassRestriction || (c.ClassRestriction&(1<<classID)) != 0
|
|
}
|
|
|
|
// IsInChannel checks if a character is in the channel
|
|
func (c *Channel) IsInChannel(characterID int32) bool {
|
|
return c.isInChannel(characterID)
|
|
}
|
|
|
|
// isInChannel is the internal implementation without locking
|
|
func (c *Channel) isInChannel(characterID int32) bool {
|
|
return slices.Contains(c.members, characterID)
|
|
}
|
|
|
|
// JoinChannel adds a character to the channel
|
|
func (c *Channel) JoinChannel(characterID int32) error {
|
|
return c.joinChannel(characterID)
|
|
}
|
|
|
|
// joinChannel is the internal implementation without locking
|
|
func (c *Channel) joinChannel(characterID int32) error {
|
|
// Check if already in channel
|
|
if c.isInChannel(characterID) {
|
|
return fmt.Errorf("character %d is already in channel %s", characterID, c.Name)
|
|
}
|
|
|
|
// Add to members list
|
|
c.members = append(c.members, characterID)
|
|
return nil
|
|
}
|
|
|
|
// LeaveChannel removes a character from the channel
|
|
func (c *Channel) LeaveChannel(characterID int32) error {
|
|
return c.leaveChannel(characterID)
|
|
}
|
|
|
|
// leaveChannel is the internal implementation without locking
|
|
func (c *Channel) leaveChannel(characterID int32) error {
|
|
// Find and remove the character
|
|
for i, memberID := range c.members {
|
|
if memberID == characterID {
|
|
// Remove member by swapping with last element and truncating
|
|
c.members[i] = c.members[len(c.members)-1]
|
|
c.members = c.members[:len(c.members)-1]
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("character %d is not in channel %s", characterID, c.Name)
|
|
}
|
|
|
|
// GetMembers returns a copy of the current member list
|
|
func (c *Channel) GetMembers() []int32 {
|
|
// Return a copy to prevent external modification
|
|
members := make([]int32, len(c.members))
|
|
copy(members, c.members)
|
|
return members
|
|
}
|
|
|
|
// GetChannelInfo returns basic channel information
|
|
func (c *Channel) GetChannelInfo() ChannelInfo {
|
|
return ChannelInfo{
|
|
Name: c.Name,
|
|
HasPassword: c.Password != "",
|
|
MemberCount: len(c.members),
|
|
LevelRestriction: c.LevelRestriction,
|
|
RaceRestriction: c.RaceRestriction,
|
|
ClassRestriction: c.ClassRestriction,
|
|
ChannelType: c.ChannelType,
|
|
}
|
|
}
|
|
|
|
// ValidateJoin checks if a character can join the channel based on restrictions
|
|
func (c *Channel) ValidateJoin(level, race, class int32, password string) error {
|
|
// Check password
|
|
if c.Password != "" && c.Password != password {
|
|
return fmt.Errorf("invalid password for channel %s", c.Name)
|
|
}
|
|
|
|
// Check level restriction
|
|
if !c.CanJoinChannelByLevel(level) {
|
|
return fmt.Errorf("level %d does not meet minimum requirement of %d for channel %s",
|
|
level, c.LevelRestriction, c.Name)
|
|
}
|
|
|
|
// Check race restriction
|
|
if !c.CanJoinChannelByRace(race) {
|
|
return fmt.Errorf("race %d is not allowed in channel %s", race, c.Name)
|
|
}
|
|
|
|
// Check class restriction
|
|
if !c.CanJoinChannelByClass(class) {
|
|
return fmt.Errorf("class %d is not allowed in channel %s", class, c.Name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsEmpty returns true if the channel has no members
|
|
func (c *Channel) IsEmpty() bool {
|
|
return len(c.members) == 0
|
|
}
|
|
|
|
// Copy creates a deep copy of the channel (useful for serialization or backups)
|
|
func (c *Channel) Copy() *Channel {
|
|
newChannel := &Channel{
|
|
ID: c.ID,
|
|
Name: c.Name,
|
|
Password: c.Password,
|
|
ChannelType: c.ChannelType,
|
|
LevelRestriction: c.LevelRestriction,
|
|
RaceRestriction: c.RaceRestriction,
|
|
ClassRestriction: c.ClassRestriction,
|
|
DiscordEnabled: c.DiscordEnabled,
|
|
Created: c.Created,
|
|
Updated: c.Updated,
|
|
db: c.db,
|
|
isNew: true, // Copy is always new
|
|
members: make([]int32, len(c.members)),
|
|
}
|
|
|
|
copy(newChannel.members, c.members)
|
|
return newChannel
|
|
}
|
|
|
|
// IsNew returns true if this is a new channel not yet saved to database
|
|
func (c *Channel) IsNew() bool {
|
|
return c.isNew
|
|
}
|
|
|
|
// Save saves the channel to the database
|
|
func (c *Channel) Save() error {
|
|
if c.db == nil {
|
|
return fmt.Errorf("no database connection available")
|
|
}
|
|
|
|
if c.isNew {
|
|
return c.insert()
|
|
}
|
|
return c.update()
|
|
}
|
|
|
|
// Delete removes the channel from the database
|
|
func (c *Channel) Delete() error {
|
|
if c.db == nil {
|
|
return fmt.Errorf("no database connection available")
|
|
}
|
|
|
|
if c.isNew {
|
|
return fmt.Errorf("cannot delete unsaved channel")
|
|
}
|
|
|
|
query := `DELETE FROM channels WHERE id = ?`
|
|
_, err := c.db.Exec(query, c.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete channel %d: %w", c.ID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reload reloads the channel data from the database
|
|
func (c *Channel) Reload() error {
|
|
if c.db == nil {
|
|
return fmt.Errorf("no database connection available")
|
|
}
|
|
|
|
if c.isNew {
|
|
return fmt.Errorf("cannot reload unsaved channel")
|
|
}
|
|
|
|
query := `SELECT name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels WHERE id = ?`
|
|
row := c.db.QueryRow(query, c.ID)
|
|
|
|
err := row.Scan(&c.Name, &c.Password, &c.ChannelType, &c.LevelRestriction,
|
|
&c.RaceRestriction, &c.ClassRestriction, &c.DiscordEnabled, &c.Created, &c.Updated)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to reload channel %d: %w", c.ID, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// insert inserts a new channel into the database
|
|
func (c *Channel) insert() error {
|
|
query := `INSERT INTO channels (name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
result, err := c.db.Exec(query, c.Name, c.Password, c.ChannelType, c.LevelRestriction,
|
|
c.RaceRestriction, c.ClassRestriction, c.DiscordEnabled, c.Created, c.Updated)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert channel: %w", err)
|
|
}
|
|
|
|
// Get the generated ID
|
|
id, err := result.LastInsertId()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get inserted channel ID: %w", err)
|
|
}
|
|
c.ID = int32(id)
|
|
c.isNew = false
|
|
return nil
|
|
}
|
|
|
|
// update updates an existing channel in the database
|
|
func (c *Channel) update() error {
|
|
c.Updated = time.Now()
|
|
query := `UPDATE channels SET name = ?, password = ?, type = ?, level_restriction = ?,
|
|
race_restriction = ?, class_restriction = ?, discord_enabled = ?, updated_at = ?
|
|
WHERE id = ?`
|
|
result, err := c.db.Exec(query, c.Name, c.Password, c.ChannelType, c.LevelRestriction,
|
|
c.RaceRestriction, c.ClassRestriction, c.DiscordEnabled, c.Updated, c.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update channel: %w", err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get rows affected: %w", err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return fmt.Errorf("channel %d not found for update", c.ID)
|
|
}
|
|
|
|
return nil
|
|
}
|