510 lines
14 KiB
Go
510 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ServicesManager handles all network services
|
|
type ServicesManager struct {
|
|
server *Server
|
|
config *ServicesConfig
|
|
nickServ *NickServ
|
|
chanServ *ChanServ
|
|
operServ *OperServ
|
|
memoServ *MemoServ
|
|
enabled bool
|
|
}
|
|
|
|
// NickServ handles nickname registration and authentication
|
|
type NickServ struct {
|
|
manager *ServicesManager
|
|
nick string
|
|
accounts map[string]*NickAccount
|
|
}
|
|
|
|
// ChanServ handles channel registration and management
|
|
type ChanServ struct {
|
|
manager *ServicesManager
|
|
nick string
|
|
channels map[string]*RegisteredChannel
|
|
}
|
|
|
|
// OperServ handles network operator services
|
|
type OperServ struct {
|
|
manager *ServicesManager
|
|
nick string
|
|
}
|
|
|
|
// MemoServ handles user-to-user messages
|
|
type MemoServ struct {
|
|
manager *ServicesManager
|
|
nick string
|
|
memos map[string][]*Memo
|
|
}
|
|
|
|
// NickAccount represents a registered nickname
|
|
type NickAccount struct {
|
|
Nick string `json:"nick"`
|
|
PasswordHash string `json:"password_hash"`
|
|
Email string `json:"email"`
|
|
RegisterTime time.Time `json:"register_time"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
Settings map[string]string `json:"settings"`
|
|
AccessList []string `json:"access_list"`
|
|
Flags []string `json:"flags"`
|
|
}
|
|
|
|
// RegisteredChannel represents a registered channel
|
|
type RegisteredChannelServices struct {
|
|
Name string `json:"name"`
|
|
Founder string `json:"founder"`
|
|
RegisterTime time.Time `json:"register_time"`
|
|
Topic string `json:"topic"`
|
|
Modes string `json:"modes"`
|
|
AccessList map[string]*ChannelAccess `json:"access_list"`
|
|
Settings map[string]string `json:"settings"`
|
|
AutoModes map[string]string `json:"auto_modes"`
|
|
}
|
|
|
|
// ChannelAccess represents access levels for channels - using the one from database.go
|
|
// type ChannelAccess struct {
|
|
// Nick string `json:"nick"`
|
|
// Level int `json:"level"` // 0=banned, 1=voice, 2=halfop, 3=op, 4=sop, 5=founder
|
|
// SetBy string `json:"set_by"`
|
|
// SetTime time.Time `json:"set_time"`
|
|
// LastUsed time.Time `json:"last_used"`
|
|
// }
|
|
|
|
// Memo represents a user memo
|
|
type Memo struct {
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Message string `json:"message"`
|
|
Time time.Time `json:"time"`
|
|
Read bool `json:"read"`
|
|
}
|
|
|
|
// ServicesConfig configuration for services
|
|
type ServicesConfig struct {
|
|
Enable bool `json:"enable"`
|
|
NickServ NickServConfig `json:"nickserv"`
|
|
ChanServ ChanServConfig `json:"chanserv"`
|
|
OperServ OperServConfig `json:"operserv"`
|
|
MemoServ MemoServConfig `json:"memoserv"`
|
|
Database DatabaseConfig `json:"database"`
|
|
}
|
|
|
|
type NickServConfig struct {
|
|
Enable bool `json:"enable"`
|
|
Nick string `json:"nick"`
|
|
User string `json:"user"`
|
|
Host string `json:"host"`
|
|
Realname string `json:"realname"`
|
|
ExpireTime int `json:"expire_time"` // Days until unused nicks expire
|
|
IdentifyTimeout int `json:"identify_timeout"` // Seconds to identify before kill
|
|
EmailVerify bool `json:"email_verify"`
|
|
RestrictReg bool `json:"restrict_registration"`
|
|
}
|
|
|
|
type ChanServConfig struct {
|
|
Enable bool `json:"enable"`
|
|
Nick string `json:"nick"`
|
|
User string `json:"user"`
|
|
Host string `json:"host"`
|
|
Realname string `json:"realname"`
|
|
ExpireTime int `json:"expire_time"` // Days until unused channels expire
|
|
MaxChannels int `json:"max_channels"` // Max channels per user
|
|
AutoDeop bool `json:"auto_deop"` // Auto-deop users without access
|
|
}
|
|
|
|
type OperServConfig struct {
|
|
Enable bool `json:"enable"`
|
|
Nick string `json:"nick"`
|
|
User string `json:"user"`
|
|
Host string `json:"host"`
|
|
Realname string `json:"realname"`
|
|
}
|
|
|
|
type MemoServConfig struct {
|
|
Enable bool `json:"enable"`
|
|
Nick string `json:"nick"`
|
|
User string `json:"user"`
|
|
Host string `json:"host"`
|
|
Realname string `json:"realname"`
|
|
MaxMemos int `json:"max_memos"`
|
|
MemoExpire int `json:"memo_expire"` // Days until memos expire
|
|
}
|
|
|
|
// NewServicesManager creates a new services manager
|
|
func NewServicesManager(server *Server, config *ServicesConfig) *ServicesManager {
|
|
sm := &ServicesManager{
|
|
server: server,
|
|
config: config,
|
|
enabled: config.Enable,
|
|
}
|
|
|
|
if config.NickServ.Enable {
|
|
sm.nickServ = &NickServ{
|
|
manager: sm,
|
|
nick: config.NickServ.Nick,
|
|
accounts: make(map[string]*NickAccount),
|
|
}
|
|
}
|
|
|
|
if config.ChanServ.Enable {
|
|
sm.chanServ = &ChanServ{
|
|
manager: sm,
|
|
nick: config.ChanServ.Nick,
|
|
channels: make(map[string]*RegisteredChannel),
|
|
}
|
|
}
|
|
|
|
if config.OperServ.Enable {
|
|
sm.operServ = &OperServ{
|
|
manager: sm,
|
|
nick: config.OperServ.Nick,
|
|
}
|
|
}
|
|
|
|
if config.MemoServ.Enable {
|
|
sm.memoServ = &MemoServ{
|
|
manager: sm,
|
|
nick: config.MemoServ.Nick,
|
|
memos: make(map[string][]*Memo),
|
|
}
|
|
}
|
|
|
|
return sm
|
|
}
|
|
|
|
// Start initializes and starts all enabled services
|
|
func (sm *ServicesManager) Start() error {
|
|
if !sm.enabled {
|
|
return nil
|
|
}
|
|
|
|
log.Println("Starting TechIRCd Services...")
|
|
|
|
// Create service clients
|
|
if sm.nickServ != nil {
|
|
sm.createServiceClient(sm.config.NickServ.Nick, sm.config.NickServ.User,
|
|
sm.config.NickServ.Host, sm.config.NickServ.Realname)
|
|
}
|
|
|
|
if sm.chanServ != nil {
|
|
sm.createServiceClient(sm.config.ChanServ.Nick, sm.config.ChanServ.User,
|
|
sm.config.ChanServ.Host, sm.config.ChanServ.Realname)
|
|
}
|
|
|
|
if sm.operServ != nil {
|
|
sm.createServiceClient(sm.config.OperServ.Nick, sm.config.OperServ.User,
|
|
sm.config.OperServ.Host, sm.config.OperServ.Realname)
|
|
}
|
|
|
|
if sm.memoServ != nil {
|
|
sm.createServiceClient(sm.config.MemoServ.Nick, sm.config.MemoServ.User,
|
|
sm.config.MemoServ.Host, sm.config.MemoServ.Realname)
|
|
}
|
|
|
|
log.Println("Services started successfully")
|
|
return nil
|
|
}
|
|
|
|
// createServiceClient creates a virtual client for a service
|
|
func (sm *ServicesManager) createServiceClient(nick, user, host, realname string) {
|
|
// Create a virtual service client that appears as a regular user
|
|
serviceClient := &Client{
|
|
nick: nick,
|
|
user: user,
|
|
host: host,
|
|
realname: realname,
|
|
server: sm.server,
|
|
modes: make(map[rune]bool),
|
|
channels: make(map[string]*Channel),
|
|
capabilities: make(map[string]bool),
|
|
}
|
|
|
|
// Set service modes
|
|
serviceClient.modes['S'] = true // Service mode
|
|
serviceClient.modes['o'] = true // Operator mode
|
|
|
|
// Add to server
|
|
sm.server.clients[strings.ToLower(nick)] = serviceClient
|
|
|
|
// Send introduction to network
|
|
sm.server.BroadcastToServers(fmt.Sprintf(":%s NICK %s", sm.server.config.Server.Name, nick))
|
|
}
|
|
|
|
// HandleMessage processes messages directed to services
|
|
func (sm *ServicesManager) HandleMessage(from *Client, target, message string) bool {
|
|
if !sm.enabled {
|
|
return false
|
|
}
|
|
|
|
lowerTarget := strings.ToLower(target)
|
|
parts := strings.Fields(message)
|
|
if len(parts) == 0 {
|
|
return false
|
|
}
|
|
|
|
command := strings.ToUpper(parts[0])
|
|
|
|
switch lowerTarget {
|
|
case strings.ToLower(sm.config.NickServ.Nick):
|
|
return sm.handleNickServCommand(from, command, parts[1:])
|
|
case strings.ToLower(sm.config.ChanServ.Nick):
|
|
return sm.handleChanServCommand(from)
|
|
case strings.ToLower(sm.config.OperServ.Nick):
|
|
return sm.handleOperServCommand(from)
|
|
case strings.ToLower(sm.config.MemoServ.Nick):
|
|
return sm.handleMemoServCommand(from)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// NickServ command handlers
|
|
func (sm *ServicesManager) handleNickServCommand(from *Client, command string, args []string) bool {
|
|
switch command {
|
|
case "REGISTER":
|
|
return sm.handleNickServRegister(from, args)
|
|
case "IDENTIFY":
|
|
return sm.handleNickServIdentify(from, args)
|
|
case "INFO":
|
|
return sm.handleNickServInfo(from, args)
|
|
case "DROP":
|
|
return sm.handleNickServDrop(from, args)
|
|
case "SET":
|
|
return sm.handleNickServSet(from)
|
|
case "ACCESS":
|
|
return sm.handleNickServAccess(from)
|
|
case "HELP":
|
|
return sm.handleNickServHelp(from)
|
|
default:
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("Unknown command %s. Type /msg %s HELP for help.", command, sm.config.NickServ.Nick))
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServRegister(from *Client, args []string) bool {
|
|
if len(args) < 2 {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"Syntax: REGISTER <password> <email>")
|
|
return true
|
|
}
|
|
|
|
nick := strings.ToLower(from.nick)
|
|
password := args[0]
|
|
email := args[1]
|
|
|
|
// Check if nick is already registered
|
|
if _, exists := sm.nickServ.accounts[nick]; exists {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"This nickname is already registered.")
|
|
return true
|
|
}
|
|
|
|
// Hash password
|
|
hash := sha256.Sum256([]byte(password))
|
|
passwordHash := hex.EncodeToString(hash[:])
|
|
|
|
// Create account
|
|
account := &NickAccount{
|
|
Nick: from.nick,
|
|
PasswordHash: passwordHash,
|
|
Email: email,
|
|
RegisterTime: time.Now(),
|
|
LastSeen: time.Now(),
|
|
Settings: make(map[string]string),
|
|
AccessList: []string{},
|
|
Flags: []string{},
|
|
}
|
|
|
|
sm.nickServ.accounts[nick] = account
|
|
|
|
// Set user as identified
|
|
from.account = from.nick
|
|
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("Nickname %s has been registered successfully.", from.nick))
|
|
|
|
return true
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServIdentify(from *Client, args []string) bool {
|
|
if len(args) < 1 {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"Syntax: IDENTIFY <password>")
|
|
return true
|
|
}
|
|
|
|
nick := strings.ToLower(from.nick)
|
|
password := args[0]
|
|
|
|
account, exists := sm.nickServ.accounts[nick]
|
|
if !exists {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"This nickname is not registered.")
|
|
return true
|
|
}
|
|
|
|
// Check password
|
|
hash := sha256.Sum256([]byte(password))
|
|
passwordHash := hex.EncodeToString(hash[:])
|
|
|
|
if account.PasswordHash != passwordHash {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"Invalid password.")
|
|
return true
|
|
}
|
|
|
|
// Set user as identified
|
|
from.account = from.nick
|
|
account.LastSeen = time.Now()
|
|
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("You are now identified for %s.", from.nick))
|
|
|
|
return true
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServInfo(from *Client, args []string) bool {
|
|
var targetNick string
|
|
if len(args) > 0 {
|
|
targetNick = args[0]
|
|
} else {
|
|
targetNick = from.nick
|
|
}
|
|
|
|
nick := strings.ToLower(targetNick)
|
|
account, exists := sm.nickServ.accounts[nick]
|
|
if !exists {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("The nickname %s is not registered.", targetNick))
|
|
return true
|
|
}
|
|
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("Information for %s:", account.Nick))
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("Registered: %s", account.RegisterTime.Format("Jan 02, 2006 15:04:05 MST")))
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("Last seen: %s", account.LastSeen.Format("Jan 02, 2006 15:04:05 MST")))
|
|
|
|
return true
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServDrop(from *Client, _ []string) bool {
|
|
nick := strings.ToLower(from.nick)
|
|
|
|
if from.account != from.nick {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"You must be identified to drop your nickname.")
|
|
return true
|
|
}
|
|
|
|
delete(sm.nickServ.accounts, nick)
|
|
from.account = ""
|
|
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
fmt.Sprintf("Nickname %s has been dropped.", from.nick))
|
|
|
|
return true
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServSet(from *Client) bool {
|
|
// Handle SET commands (EMAIL, PASSWORD, etc.)
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"SET command not yet implemented.")
|
|
return true
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServAccess(from *Client) bool {
|
|
// Handle ACCESS commands (ADD, DEL, LIST)
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
|
|
"ACCESS command not yet implemented.")
|
|
return true
|
|
}
|
|
|
|
func (sm *ServicesManager) handleNickServHelp(from *Client) bool {
|
|
help := []string{
|
|
"*** NickServ Help ***",
|
|
"REGISTER <password> <email> - Register your nickname",
|
|
"IDENTIFY <password> - Identify to your nickname",
|
|
"INFO [nick] - Show information about a nickname",
|
|
"DROP - Drop your nickname registration",
|
|
"SET - Change settings",
|
|
"ACCESS - Manage access list",
|
|
"HELP - Show this help",
|
|
}
|
|
|
|
for _, line := range help {
|
|
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick, line)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ChanServ command handlers (placeholder)
|
|
func (sm *ServicesManager) handleChanServCommand(from *Client) bool {
|
|
sm.sendServiceNotice(sm.config.ChanServ.Nick, from.nick,
|
|
"ChanServ commands not yet implemented.")
|
|
return true
|
|
}
|
|
|
|
// OperServ command handlers (placeholder)
|
|
func (sm *ServicesManager) handleOperServCommand(from *Client) bool {
|
|
sm.sendServiceNotice(sm.config.OperServ.Nick, from.nick,
|
|
"OperServ commands not yet implemented.")
|
|
return true
|
|
}
|
|
|
|
// MemoServ command handlers (placeholder)
|
|
func (sm *ServicesManager) handleMemoServCommand(from *Client) bool {
|
|
sm.sendServiceNotice(sm.config.MemoServ.Nick, from.nick,
|
|
"MemoServ commands not yet implemented.")
|
|
return true
|
|
}
|
|
|
|
// sendServiceNotice sends a notice from a service to a user
|
|
func (sm *ServicesManager) sendServiceNotice(from, to, message string) {
|
|
if client := sm.server.GetClient(to); client != nil {
|
|
client.SendMessage(fmt.Sprintf(":%s NOTICE %s :%s", from, to, message))
|
|
}
|
|
}
|
|
|
|
// IsServiceNick checks if a nick is a service
|
|
func (sm *ServicesManager) IsServiceNick(nick string) bool {
|
|
if !sm.enabled {
|
|
return false
|
|
}
|
|
|
|
lowerNick := strings.ToLower(nick)
|
|
return lowerNick == strings.ToLower(sm.config.NickServ.Nick) ||
|
|
lowerNick == strings.ToLower(sm.config.ChanServ.Nick) ||
|
|
lowerNick == strings.ToLower(sm.config.OperServ.Nick) ||
|
|
lowerNick == strings.ToLower(sm.config.MemoServ.Nick)
|
|
}
|
|
|
|
// GetUserAccount returns the account for a nick
|
|
func (sm *ServicesManager) GetUserAccount(nick string) *NickAccount {
|
|
if sm.nickServ == nil {
|
|
return nil
|
|
}
|
|
return sm.nickServ.accounts[strings.ToLower(nick)]
|
|
}
|
|
|
|
// IsIdentified checks if a user is identified
|
|
func (sm *ServicesManager) IsIdentified(nick string) bool {
|
|
if client := sm.server.GetClient(nick); client != nil {
|
|
return client.account != ""
|
|
}
|
|
return false
|
|
}
|