Compare commits
4 Commits
d340bc49da
...
main
Author | SHA1 | Date | |
---|---|---|---|
c3a7f7487c | |||
1b099c400a | |||
5b0b6a48a9 | |||
5b29c26102 |
39
src/irc/actionTypes.go
Normal file
39
src/irc/actionTypes.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
type NickChangeAction struct {
|
||||||
|
User *User
|
||||||
|
OldNick *Nick
|
||||||
|
NewNick *Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartAction struct {
|
||||||
|
User *User
|
||||||
|
ChannelName ChannelName
|
||||||
|
Reason *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type JoinAction struct {
|
||||||
|
User *User
|
||||||
|
ChannelName ChannelName
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatModePrivmsg ChatMode = "PRIVMSG"
|
||||||
|
ChatModeNotice ChatMode = "NOTICE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SendMessageToUserAction struct {
|
||||||
|
User *User
|
||||||
|
Mode ChatMode
|
||||||
|
Destination *User
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendMessageToChannelAction struct {
|
||||||
|
User *User
|
||||||
|
Mode ChatMode
|
||||||
|
Destination canonicalChannelName
|
||||||
|
Message string
|
||||||
|
}
|
46
src/irc/broadcast.go
Normal file
46
src/irc/broadcast.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BroadcastGroup struct {
|
||||||
|
channels []canonicalChannelName
|
||||||
|
users []*User
|
||||||
|
specificallyExcludedUsers []*User
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBroadcastGroup() *BroadcastGroup {
|
||||||
|
return &BroadcastGroup{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bcg *BroadcastGroup) AddChannels(names ...canonicalChannelName) {
|
||||||
|
bcg.channels = append(bcg.channels, names...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bcg *BroadcastGroup) AddUsers(users ...*User) {
|
||||||
|
bcg.users = append(bcg.users, users...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bcg *BroadcastGroup) AddSpecificallyExcludedUsers(users ...*User) {
|
||||||
|
bcg.specificallyExcludedUsers = append(bcg.users, users...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Broadcast(bcg *BroadcastGroup, content transport.Content) {
|
||||||
|
g := GetGlobals()
|
||||||
|
allUsers := make(map[*User]struct{})
|
||||||
|
for _, c := range bcg.channels {
|
||||||
|
for u := range g.Users.ByCanonicalChannel(c) {
|
||||||
|
allUsers[u] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, u := range bcg.users {
|
||||||
|
allUsers[u] = struct{}{}
|
||||||
|
}
|
||||||
|
for _, u := range bcg.specificallyExcludedUsers {
|
||||||
|
delete(allUsers, u)
|
||||||
|
}
|
||||||
|
for u := range allUsers {
|
||||||
|
g.Server.SendMessage(u.clientId, content)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/world"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HandleCommands(msg world.WrappedMessage) {
|
|
||||||
handleAuthCommands(msg)
|
|
||||||
handleJoinPartCommands(msg)
|
|
||||||
handlePrivmsgNotifyCommands(msg)
|
|
||||||
}
|
|
@@ -1,84 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/users"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/world"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleAuthCommands(msg world.WrappedMessage) {
|
|
||||||
handleNickAndUser(msg)
|
|
||||||
completeHandshakeIfPossible(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNickAndUser(msg world.WrappedMessage) {
|
|
||||||
if msg.Sender.GetHasReceivedAuthHandshakeReply() {
|
|
||||||
// TODO: Send an error reply
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Content.Command == "NICK" {
|
|
||||||
args := msg.Content.Arguments
|
|
||||||
if len(args) != 1 {
|
|
||||||
// TODO: Send an error reply
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nick := args[0]
|
|
||||||
validNick, err := users.ValidateNick(nick)
|
|
||||||
if err != nil {
|
|
||||||
// TODO: Send an error reply
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = msg.Sender.SetNick(&validNick)
|
|
||||||
if err != nil {
|
|
||||||
msg.World.Server.TerminateClient(msg.Sender.GetClientId(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Content.Command == "USER" {
|
|
||||||
args := msg.Content.Arguments
|
|
||||||
if len(args) != 4 {
|
|
||||||
// TODO: Send an error reply
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
username := args[0]
|
|
||||||
zero := args[1]
|
|
||||||
star := args[2]
|
|
||||||
realName := args[3]
|
|
||||||
|
|
||||||
if zero != "0" || star != "*" {
|
|
||||||
// TODO: Send an error reply
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Sender.SetUsername(&username)
|
|
||||||
msg.Sender.SetRealName(&realName)
|
|
||||||
|
|
||||||
// TODO: Validation? I wonder if it matters.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func completeHandshakeIfPossible(msg world.WrappedMessage) {
|
|
||||||
sender := msg.Sender
|
|
||||||
if msg.Sender.GetHasReceivedAuthHandshakeReply() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isReady := sender.GetNick() != nil && sender.GetUsername() != nil && sender.GetRealName() != nil
|
|
||||||
if !isReady {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sender.SetHasReceivedAuthHandshakeReply(true)
|
|
||||||
msg.World.Server.SendMessage(sender.GetClientId(), transport.Content{
|
|
||||||
Command: "NICK",
|
|
||||||
Arguments: []string{sender.GetNick().Value},
|
|
||||||
})
|
|
||||||
msg.World.Server.SendMessage(msg.Sender.GetClientId(), transport.Content{
|
|
||||||
Command: "USER",
|
|
||||||
Arguments: []string{*sender.GetUsername(), "0", "*", *sender.GetRealName()},
|
|
||||||
})
|
|
||||||
}
|
|
@@ -1,59 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/users"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/world"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleJoinPartCommands(msg world.WrappedMessage) {
|
|
||||||
if msg.Content.Command == "JOIN" {
|
|
||||||
if len(msg.Content.Arguments) != 1 {
|
|
||||||
// TODO: Wrong number of arguments
|
|
||||||
return
|
|
||||||
}
|
|
||||||
channelsToJoin := parseChannelList(msg.Content.Arguments[0])
|
|
||||||
|
|
||||||
for _, channel := range channelsToJoin {
|
|
||||||
err := msg.Sender.Join(channel)
|
|
||||||
if err != nil {
|
|
||||||
msg.World.Server.TerminateClient(msg.Sender.GetClientId(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg.World.RelayToChannel(msg, channel, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Content.Command == "PART" {
|
|
||||||
n := len(msg.Content.Arguments)
|
|
||||||
if !(n == 1 || n == 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
channelsToPart := parseChannelList(msg.Content.Arguments[0])
|
|
||||||
|
|
||||||
for _, channel := range channelsToPart {
|
|
||||||
err := msg.Sender.Part(channel)
|
|
||||||
if err != nil {
|
|
||||||
msg.World.Server.TerminateClient(msg.Sender.GetClientId(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg.World.RelayToChannel(msg, channel, nil)
|
|
||||||
// the user won't see their own #part because they left, so send it
|
|
||||||
msg.World.RelayToClient(msg, msg.Sender.GetClientId(), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseChannelList(arg string) []users.ChannelName {
|
|
||||||
var channels []users.ChannelName
|
|
||||||
for _, channelName := range strings.Split(arg, ",") {
|
|
||||||
validChannel, err := users.ValidateChannelName(channelName)
|
|
||||||
if err != nil { // can't join, not a channel
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
channels = append(channels, validChannel)
|
|
||||||
}
|
|
||||||
return channels
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/world"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handlePrivmsgNotifyCommands(msg world.WrappedMessage) {
|
|
||||||
if msg.Content.Command == "PRIVMSG" || msg.Content.Command == "NOTIFY" || msg.Content.Command == "CTCP" {
|
|
||||||
log.Printf("message-like command")
|
|
||||||
if len(msg.Content.Arguments) == 0 {
|
|
||||||
// TODO: Error reply
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Was this message to a user?
|
|
||||||
msg.World.RelayToVagueDestination(msg, msg.Content.Arguments[0], []transport.ClientId{msg.Sender.GetClientId()})
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +1,9 @@
|
|||||||
package errors
|
package irc
|
||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
var ErrAlreadyInChannel = fmt.Errorf("already in channel")
|
var ErrAlreadyInChannel = fmt.Errorf("already in channel")
|
||||||
|
var ErrMalformedCommand = fmt.Errorf("malformed command")
|
||||||
|
var ErrNotANick = fmt.Errorf("does not look like a nickname")
|
||||||
var ErrNickAlreadyInUse = fmt.Errorf("nick already in use")
|
var ErrNickAlreadyInUse = fmt.Errorf("nick already in use")
|
||||||
var ErrNotInChannel = fmt.Errorf("not in channel")
|
var ErrNotInChannel = fmt.Errorf("not in channel")
|
41
src/irc/globals.go
Normal file
41
src/irc/globals.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/logic"
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Globals struct {
|
||||||
|
Server *transport.Server
|
||||||
|
Rulebook *logic.Rulebook
|
||||||
|
Users *UsersSystem
|
||||||
|
Notifications *NotificationsSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
var globals *Globals = nil
|
||||||
|
|
||||||
|
func InitializeGlobals(server *transport.Server) {
|
||||||
|
globals = &Globals{
|
||||||
|
Server: server,
|
||||||
|
Rulebook: logic.NewRulebook(),
|
||||||
|
Users: NewUsersSystem(),
|
||||||
|
Notifications: NewNotificationsSystem(),
|
||||||
|
}
|
||||||
|
globals.Users.InitializeRules()
|
||||||
|
globals.Notifications.InitializeRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetGlobals() *Globals {
|
||||||
|
if globals == nil {
|
||||||
|
panic("globals not initialized")
|
||||||
|
}
|
||||||
|
return globals
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddNewRule[T any](check func(T) error, carryOut func(T)) {
|
||||||
|
GetGlobals().Rulebook.Add(logic.NewRule(check, carryOut))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Perform[T any](action T) error {
|
||||||
|
return GetGlobals().Rulebook.Perform(action)
|
||||||
|
}
|
@@ -1,61 +0,0 @@
|
|||||||
package irc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNotANick = fmt.Errorf("does not look like a nickname")
|
|
||||||
|
|
||||||
var regexpNick = regexp.MustCompile("^[a-zA-Z0-9]+$") // NOTE: more constrained than real character set
|
|
||||||
|
|
||||||
type Nick string
|
|
||||||
type CanonicalNick string
|
|
||||||
|
|
||||||
func ValidateNick(s string) (Nick, error) {
|
|
||||||
// TODO: Fail if the string doesn't look like a nick
|
|
||||||
if !regexpNick.MatchString(s) {
|
|
||||||
return "", fmt.Errorf("%w: %s", ErrNotANick, s)
|
|
||||||
}
|
|
||||||
return Nick(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Nick) Canonize() CanonicalNick {
|
|
||||||
return CanonicalNick(strings.ToLower(string(n)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Nick) CanonizeNullable() *CanonicalNick {
|
|
||||||
if n == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := n.Canonize()
|
|
||||||
return &result
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrNotAChannel = fmt.Errorf("does not look like a channel name")
|
|
||||||
|
|
||||||
var regexpChannel = regexp.MustCompile("^#[a-zA-Z0-9]+$") // NOTE: more constrained than real character set
|
|
||||||
|
|
||||||
type Channel string
|
|
||||||
type CanonicalChannel string
|
|
||||||
|
|
||||||
func ValidateChannel(s string) (Channel, error) {
|
|
||||||
// TODO: Fail if the string doesn't look like a channel name
|
|
||||||
if !regexpChannel.MatchString(s) {
|
|
||||||
return "", fmt.Errorf("%w: %s", ErrNotAChannel, s)
|
|
||||||
}
|
|
||||||
return Channel(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Channel) Canonize() CanonicalChannel {
|
|
||||||
return CanonicalChannel(strings.ToLower(string(c)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Channel) CanonizeNullable() *CanonicalChannel {
|
|
||||||
if c == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
result := c.Canonize()
|
|
||||||
return &result
|
|
||||||
}
|
|
182
src/irc/main.go
182
src/irc/main.go
@@ -1,15 +1,15 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/commands"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/world"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServeIrc(server *transport.Server) {
|
func ServeIrc(server *transport.Server) {
|
||||||
world := world.NewWorld(server)
|
InitializeGlobals(server)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
rawMessage, err := server.ReceiveMessage()
|
rawMessage, err := server.ReceiveMessage()
|
||||||
@@ -18,8 +18,180 @@ func ServeIrc(server *transport.Server) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wrappedMessage := world.Wrap(rawMessage)
|
err = handleMessage(rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
server.TerminateClient(rawMessage.Sender, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
commands.HandleCommands(wrappedMessage)
|
func handleMessage(m transport.IncomingMessage) error {
|
||||||
|
g := GetGlobals()
|
||||||
|
user := g.Users.ByClientIdOrCreate(m.Sender)
|
||||||
|
command := m.Content.Command
|
||||||
|
args := m.Content.Arguments
|
||||||
|
|
||||||
|
if command == "NICK" {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("%w: needs 1 argument", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
nick, err := ValidateNick(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = user.SetNick(&nick)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return completeHandshakeIfPossible(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "USER" {
|
||||||
|
if len(args) != 4 {
|
||||||
|
return fmt.Errorf("%w: needs 4 arguments", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := args[0]
|
||||||
|
zero := args[1]
|
||||||
|
star := args[2]
|
||||||
|
realName := args[3]
|
||||||
|
|
||||||
|
if zero != "0" || star != "*" {
|
||||||
|
return fmt.Errorf("%w: needs zero and star", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.SetUsername(&username)
|
||||||
|
user.SetRealName(&realName)
|
||||||
|
return completeHandshakeIfPossible(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "JOIN" {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return fmt.Errorf("%w: needs 1 argument", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsToJoin := parseChannelList(args[0])
|
||||||
|
for _, channel := range channelsToJoin {
|
||||||
|
err := user.Join(channel)
|
||||||
|
log.Printf("joining %s %v", channel, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == "PART" {
|
||||||
|
if len(args) != 1 && len(args) != 2 {
|
||||||
|
return fmt.Errorf("%w: needs 1 or 2 arguments", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsToPart := parseChannelList(args[0])
|
||||||
|
var reason *string
|
||||||
|
if len(args) == 2 {
|
||||||
|
reason = &args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, channel := range channelsToPart {
|
||||||
|
err := user.Part(channel, reason)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if command == string(ChatModePrivmsg) || command == string(ChatModeNotice) {
|
||||||
|
mode := ChatMode(command)
|
||||||
|
if len(args) != 2 {
|
||||||
|
return fmt.Errorf("%w: needs 2 arguments", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelName, err := ValidateChannelName(args[0])
|
||||||
|
if err == nil {
|
||||||
|
return handleChannelMessage(user, mode, channelName.canonical, args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
nick, err := ValidateNick(args[0])
|
||||||
|
if err == nil {
|
||||||
|
other := g.Users.ByNick(nick)
|
||||||
|
if other == nil {
|
||||||
|
// TODO: Error for missing user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return handleUserMessage(user, mode, other, args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%w: needs nick or channel name", ErrMalformedCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unrecognized command
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeHandshakeIfPossible(user *User) error {
|
||||||
|
if user.GetHasReceivedAuthHandshakeReply() {
|
||||||
|
log.Printf("has already completed handshake!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nick := user.GetNick()
|
||||||
|
username := user.GetUsername()
|
||||||
|
realName := user.GetRealName()
|
||||||
|
|
||||||
|
isReady := !(nick == nil || username == nil || realName == nil)
|
||||||
|
|
||||||
|
if !isReady {
|
||||||
|
log.Printf("not ready to complete handshake!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Printf("time to complete handshake!!")
|
||||||
|
|
||||||
|
user.SetHasReceivedAuthHandshakeReply(true)
|
||||||
|
group := NewBroadcastGroup()
|
||||||
|
group.AddUsers(user)
|
||||||
|
Broadcast(group, transport.Content{
|
||||||
|
Command: "NICK",
|
||||||
|
Arguments: []string{nick.Value},
|
||||||
|
})
|
||||||
|
Broadcast(group, transport.Content{
|
||||||
|
Command: "USER",
|
||||||
|
Arguments: []string{nick.Value, "0", "*", *realName},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseChannelList(arg string) []ChannelName {
|
||||||
|
var channels []ChannelName
|
||||||
|
for _, channelName := range strings.Split(arg, ",") {
|
||||||
|
validChannel, err := ValidateChannelName(channelName)
|
||||||
|
if err != nil { // can't join, not a channel
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = append(channels, validChannel)
|
||||||
|
}
|
||||||
|
return channels
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleChannelMessage(sender *User, mode ChatMode, destination canonicalChannelName, message string) error {
|
||||||
|
return Perform(SendMessageToChannelAction{
|
||||||
|
User: sender,
|
||||||
|
Mode: mode,
|
||||||
|
Destination: destination,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUserMessage(sender *User, mode ChatMode, destination *User, message string) error {
|
||||||
|
return Perform(SendMessageToUserAction{
|
||||||
|
User: sender,
|
||||||
|
Mode: mode,
|
||||||
|
Destination: destination,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
150
src/irc/notificationsSystem.go
Normal file
150
src/irc/notificationsSystem.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationsSystem struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationsSystem() *NotificationsSystem {
|
||||||
|
return &NotificationsSystem{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (notifications *NotificationsSystem) InitializeRules() {
|
||||||
|
// nick changes
|
||||||
|
AddNewRule(
|
||||||
|
func(nc NickChangeAction) error { return nil },
|
||||||
|
func(nc NickChangeAction) {
|
||||||
|
if nc.NewNick == nil {
|
||||||
|
return // not expressible in the IRC protocol; probably shouldn't be allowed?
|
||||||
|
}
|
||||||
|
|
||||||
|
src := nc.User.GetSourceString()
|
||||||
|
content := transport.Content{
|
||||||
|
Source: &src,
|
||||||
|
Command: "NICK",
|
||||||
|
Arguments: []string{nc.NewNick.Value},
|
||||||
|
}
|
||||||
|
group := NewBroadcastGroup()
|
||||||
|
group.AddChannels(nc.User.GetChannels()...)
|
||||||
|
group.AddUsers(nc.User)
|
||||||
|
Broadcast(group, content)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// joins
|
||||||
|
AddNewRule(
|
||||||
|
func(j JoinAction) error { return nil },
|
||||||
|
func(j JoinAction) {
|
||||||
|
src := j.User.GetSourceString()
|
||||||
|
|
||||||
|
content := transport.Content{
|
||||||
|
Source: &src,
|
||||||
|
Command: "JOIN",
|
||||||
|
Arguments: []string{string(j.ChannelName.Value)},
|
||||||
|
}
|
||||||
|
group := NewBroadcastGroup()
|
||||||
|
group.AddChannels(j.ChannelName.canonical)
|
||||||
|
group.AddUsers(j.User)
|
||||||
|
Broadcast(group, content)
|
||||||
|
|
||||||
|
// tell the user who is here
|
||||||
|
group = NewBroadcastGroup()
|
||||||
|
group.AddUsers(j.User)
|
||||||
|
src2 := "server"
|
||||||
|
|
||||||
|
var nameList strings.Builder
|
||||||
|
var i = 0
|
||||||
|
for user := range GetGlobals().Users.ByChannel(j.ChannelName) {
|
||||||
|
if i != 0 {
|
||||||
|
nameList.WriteString(" ")
|
||||||
|
}
|
||||||
|
nameList.WriteString(user.nick.Value)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Broadcast(group, transport.Content{
|
||||||
|
Source: &src2,
|
||||||
|
Command: "332",
|
||||||
|
Arguments: []string{
|
||||||
|
j.User.nick.Value, j.ChannelName.Value, "TODO: topic!",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Broadcast(group, transport.Content{
|
||||||
|
Source: &src2,
|
||||||
|
Command: "353",
|
||||||
|
Arguments: []string{
|
||||||
|
j.User.nick.Value, "=", j.ChannelName.Value, nameList.String(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Broadcast(group, transport.Content{
|
||||||
|
Source: &src2,
|
||||||
|
Command: "366",
|
||||||
|
Arguments: []string{
|
||||||
|
j.User.nick.Value, j.ChannelName.Value, "End of /NAMES list",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// parts
|
||||||
|
AddNewRule(
|
||||||
|
func(p PartAction) error { return nil },
|
||||||
|
func(p PartAction) {
|
||||||
|
src := p.User.GetSourceString()
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
args = append(args, string(p.ChannelName.Value))
|
||||||
|
if p.Reason != nil {
|
||||||
|
args = append(args, *p.Reason)
|
||||||
|
}
|
||||||
|
content := transport.Content{
|
||||||
|
Source: &src,
|
||||||
|
Command: "PART",
|
||||||
|
Arguments: args,
|
||||||
|
}
|
||||||
|
group := NewBroadcastGroup()
|
||||||
|
group.AddChannels(p.ChannelName.canonical)
|
||||||
|
group.AddUsers(p.User)
|
||||||
|
Broadcast(group, content)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// messages (to user)
|
||||||
|
AddNewRule(
|
||||||
|
func(s SendMessageToUserAction) error { return nil },
|
||||||
|
func(s SendMessageToUserAction) {
|
||||||
|
src := s.User.GetSourceString()
|
||||||
|
|
||||||
|
content := transport.Content{
|
||||||
|
Source: &src,
|
||||||
|
Command: string(s.Mode),
|
||||||
|
Arguments: []string{s.Destination.GetNick().Value, s.Message},
|
||||||
|
}
|
||||||
|
group := NewBroadcastGroup()
|
||||||
|
group.AddUsers(s.Destination)
|
||||||
|
Broadcast(group, content)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// messages (to channel)
|
||||||
|
AddNewRule(
|
||||||
|
func(s SendMessageToChannelAction) error { return nil },
|
||||||
|
func(s SendMessageToChannelAction) {
|
||||||
|
src := s.User.GetSourceString()
|
||||||
|
|
||||||
|
content := transport.Content{
|
||||||
|
Source: &src,
|
||||||
|
Command: string(s.Mode),
|
||||||
|
Arguments: []string{string(s.Destination), s.Message},
|
||||||
|
}
|
||||||
|
group := NewBroadcastGroup()
|
||||||
|
group.AddChannels(s.Destination)
|
||||||
|
group.AddSpecificallyExcludedUsers(s.User)
|
||||||
|
Broadcast(group, content)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package users
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,7 +12,6 @@ type Nick struct {
|
|||||||
}
|
}
|
||||||
type canonicalNick string
|
type canonicalNick string
|
||||||
|
|
||||||
var ErrNotANick = fmt.Errorf("does not look like a nickname")
|
|
||||||
var regexpNick = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) // NOTE: more constrained than real character set
|
var regexpNick = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`) // NOTE: more constrained than real character set
|
||||||
|
|
||||||
func ValidateNick(input string) (Nick, error) {
|
func ValidateNick(input string) (Nick, error) {
|
95
src/irc/user.go
Normal file
95
src/irc/user.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
clientId transport.ClientId
|
||||||
|
hostString *string
|
||||||
|
sourceString *string
|
||||||
|
nick *Nick
|
||||||
|
username *string
|
||||||
|
realName *string
|
||||||
|
|
||||||
|
hasReceivedAuthHandshakeReply bool
|
||||||
|
|
||||||
|
channels []canonicalChannelName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) recomputeSourceString() {
|
||||||
|
nick := "unknown"
|
||||||
|
if user.nick != nil {
|
||||||
|
nick = (*user.nick).Value
|
||||||
|
}
|
||||||
|
hstr := fmt.Sprintf("clients/%d", user.clientId)
|
||||||
|
sstr := fmt.Sprintf("%s!%s", nick, hstr)
|
||||||
|
user.hostString = &hstr
|
||||||
|
user.sourceString = &sstr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetClientId() transport.ClientId {
|
||||||
|
return user.clientId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetHostString() string {
|
||||||
|
return *user.hostString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetSourceString() string {
|
||||||
|
return *user.sourceString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetNick() *Nick {
|
||||||
|
return user.nick
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SetNick(newNick *Nick) error {
|
||||||
|
return Perform(NickChangeAction{User: user, OldNick: user.nick, NewNick: newNick})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetUsername() *string {
|
||||||
|
return user.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SetUsername(username *string) {
|
||||||
|
user.username = username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetRealName() *string {
|
||||||
|
return user.realName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SetRealName(realName *string) {
|
||||||
|
user.realName = realName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetHasReceivedAuthHandshakeReply() bool {
|
||||||
|
return user.hasReceivedAuthHandshakeReply
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) SetHasReceivedAuthHandshakeReply(value bool) {
|
||||||
|
user.hasReceivedAuthHandshakeReply = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) IsInChannel(channelName ChannelName) bool {
|
||||||
|
return slices.ContainsFunc(user.channels, func(existingChannel canonicalChannelName) bool {
|
||||||
|
return channelName.canonical == existingChannel
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) GetChannels() []canonicalChannelName {
|
||||||
|
return user.channels
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Join(channelName ChannelName) error {
|
||||||
|
return Perform(JoinAction{User: user, ChannelName: channelName})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (user *User) Part(channelName ChannelName, reason *string) error {
|
||||||
|
return Perform(PartAction{User: user, ChannelName: channelName, Reason: reason})
|
||||||
|
}
|
@@ -1,183 +0,0 @@
|
|||||||
package users
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/errors"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserId uint64
|
|
||||||
|
|
||||||
type UsersSystem struct {
|
|
||||||
clientIdIndex map[transport.ClientId]*User
|
|
||||||
nickIndex map[canonicalNick]*User
|
|
||||||
channelNameIndex map[canonicalChannelName](map[*User]struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
users *UsersSystem
|
|
||||||
|
|
||||||
clientId transport.ClientId
|
|
||||||
nick *Nick
|
|
||||||
username *string
|
|
||||||
realName *string
|
|
||||||
|
|
||||||
hasReceivedAuthHandshakeReply bool
|
|
||||||
|
|
||||||
channels []ChannelName
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUsersSystem() *UsersSystem {
|
|
||||||
return &UsersSystem{
|
|
||||||
clientIdIndex: make(map[transport.ClientId]*User),
|
|
||||||
nickIndex: make(map[canonicalNick]*User),
|
|
||||||
channelNameIndex: make(map[canonicalChannelName]map[*User]struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (users *UsersSystem) ByClientIdOrCreate(clientId transport.ClientId) *User {
|
|
||||||
existing, ok := users.clientIdIndex[clientId]
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
return existing
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &User{
|
|
||||||
users: users,
|
|
||||||
|
|
||||||
clientId: clientId,
|
|
||||||
nick: nil,
|
|
||||||
username: nil,
|
|
||||||
realName: nil,
|
|
||||||
|
|
||||||
hasReceivedAuthHandshakeReply: false,
|
|
||||||
|
|
||||||
channels: nil,
|
|
||||||
}
|
|
||||||
users.clientIdIndex[clientId] = user
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
func (users *UsersSystem) ByNick(nick Nick) *User {
|
|
||||||
return users.nickIndex[nick.canonical]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (users *UsersSystem) ByChannel(channelName ChannelName) map[*User]struct{} {
|
|
||||||
return users.channelNameIndex[channelName.canonical]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) GetClientId() transport.ClientId {
|
|
||||||
return user.clientId
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) GetNick() *Nick {
|
|
||||||
return user.nick
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) SetNick(newNick *Nick) error {
|
|
||||||
users := user.users
|
|
||||||
oldNick := user.nick
|
|
||||||
|
|
||||||
// check if already in use -- if so, refuse
|
|
||||||
_, alreadyInUse := users.nickIndex[newNick.canonical]
|
|
||||||
if alreadyInUse {
|
|
||||||
if oldNick != nil && newNick.canonical == oldNick.canonical {
|
|
||||||
// it's fine, this is the user who held that nick
|
|
||||||
// so continue as before
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("%w: %s", errors.ErrNickAlreadyInUse, newNick.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update indexes
|
|
||||||
if oldNick != nil {
|
|
||||||
delete(users.nickIndex, oldNick.canonical)
|
|
||||||
}
|
|
||||||
if newNick != nil {
|
|
||||||
users.nickIndex[newNick.canonical] = user
|
|
||||||
}
|
|
||||||
|
|
||||||
// update me
|
|
||||||
user.nick = newNick
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) GetUsername() *string {
|
|
||||||
return user.username
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) SetUsername(username *string) {
|
|
||||||
user.username = username
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) GetRealName() *string {
|
|
||||||
return user.realName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) SetRealName(realName *string) {
|
|
||||||
user.realName = realName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) GetHasReceivedAuthHandshakeReply() bool {
|
|
||||||
return user.hasReceivedAuthHandshakeReply
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) SetHasReceivedAuthHandshakeReply(value bool) {
|
|
||||||
user.hasReceivedAuthHandshakeReply = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) IsInChannel(channelName ChannelName) bool {
|
|
||||||
return slices.ContainsFunc(user.channels, func(existingChannel ChannelName) bool {
|
|
||||||
return channelName.canonical == existingChannel.canonical
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) Join(channelName ChannelName) error {
|
|
||||||
users := user.users
|
|
||||||
|
|
||||||
// if I'm already in this channel, don't join
|
|
||||||
if user.IsInChannel(channelName) {
|
|
||||||
return fmt.Errorf("%w: %s", errors.ErrAlreadyInChannel, channelName.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update indexes
|
|
||||||
existing, ok := users.channelNameIndex[channelName.canonical]
|
|
||||||
if !ok {
|
|
||||||
existing = make(map[*User]struct{})
|
|
||||||
users.channelNameIndex[channelName.canonical] = existing
|
|
||||||
}
|
|
||||||
_, wasInChannel := existing[user]
|
|
||||||
if wasInChannel {
|
|
||||||
panic("tried to join a channel, but I was mysteriously already in it")
|
|
||||||
}
|
|
||||||
existing[user] = struct{}{}
|
|
||||||
|
|
||||||
// update me
|
|
||||||
user.channels = append(user.channels, channelName)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) Part(channelName ChannelName) error {
|
|
||||||
users := user.users
|
|
||||||
|
|
||||||
// if i'm not in this channel, don't part
|
|
||||||
if !user.IsInChannel(channelName) {
|
|
||||||
return fmt.Errorf("%w: %s", errors.ErrNotInChannel, channelName.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update indexes
|
|
||||||
existing, ok := users.channelNameIndex[channelName.canonical]
|
|
||||||
if ok {
|
|
||||||
delete(existing, user)
|
|
||||||
if len(existing) == 0 {
|
|
||||||
delete(users.channelNameIndex, channelName.canonical)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic("tried to part from a channel, but was mysteriously absent from it")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
129
src/irc/usersSystem.go
Normal file
129
src/irc/usersSystem.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UsersSystem struct {
|
||||||
|
clientIdIndex map[transport.ClientId]*User
|
||||||
|
nickIndex map[canonicalNick]*User
|
||||||
|
channelNameIndex map[canonicalChannelName](map[*User]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUsersSystem() *UsersSystem {
|
||||||
|
return &UsersSystem{
|
||||||
|
clientIdIndex: make(map[transport.ClientId]*User),
|
||||||
|
nickIndex: make(map[canonicalNick]*User),
|
||||||
|
channelNameIndex: make(map[canonicalChannelName]map[*User]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (users *UsersSystem) ByClientIdOrCreate(clientId transport.ClientId) *User {
|
||||||
|
existing, ok := users.clientIdIndex[clientId]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &User{
|
||||||
|
clientId: clientId,
|
||||||
|
nick: nil,
|
||||||
|
username: nil,
|
||||||
|
realName: nil,
|
||||||
|
|
||||||
|
hasReceivedAuthHandshakeReply: false,
|
||||||
|
|
||||||
|
channels: nil,
|
||||||
|
}
|
||||||
|
user.recomputeSourceString()
|
||||||
|
users.clientIdIndex[clientId] = user
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (users *UsersSystem) ByNick(nick Nick) *User {
|
||||||
|
return users.nickIndex[nick.canonical]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (users *UsersSystem) ByChannel(channelName ChannelName) map[*User]struct{} {
|
||||||
|
return users.channelNameIndex[channelName.canonical]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (users *UsersSystem) ByCanonicalChannel(channelName canonicalChannelName) map[*User]struct{} {
|
||||||
|
return users.channelNameIndex[channelName]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (users *UsersSystem) InitializeRules() {
|
||||||
|
// nick changes
|
||||||
|
AddNewRule(
|
||||||
|
func(nc NickChangeAction) error {
|
||||||
|
// users can always change from one nick to the same nick
|
||||||
|
if nc.OldNick != nil && nc.NewNick != nil && nc.OldNick.canonical == nc.NewNick.canonical {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this someone else's nick?
|
||||||
|
_, ok := users.nickIndex[nc.NewNick.canonical]
|
||||||
|
if ok {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNickAlreadyInUse, nc.NewNick.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
},
|
||||||
|
func(nc NickChangeAction) {
|
||||||
|
if nc.OldNick != nil {
|
||||||
|
delete(users.nickIndex, nc.OldNick.canonical)
|
||||||
|
}
|
||||||
|
nc.User.nick = nc.NewNick
|
||||||
|
nc.User.recomputeSourceString()
|
||||||
|
|
||||||
|
if nc.NewNick != nil {
|
||||||
|
users.nickIndex[nc.NewNick.canonical] = nc.User
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// joining channel
|
||||||
|
AddNewRule(
|
||||||
|
func(j JoinAction) error {
|
||||||
|
if slices.Contains(j.User.channels, j.ChannelName.canonical) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAlreadyInChannel, j.ChannelName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(j JoinAction) {
|
||||||
|
name := j.ChannelName.canonical
|
||||||
|
existing, ok := users.channelNameIndex[name]
|
||||||
|
if !ok {
|
||||||
|
existing = make(map[*User]struct{})
|
||||||
|
users.channelNameIndex[name] = existing
|
||||||
|
}
|
||||||
|
existing[j.User] = struct{}{}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// parting channel
|
||||||
|
AddNewRule(
|
||||||
|
func(p PartAction) error {
|
||||||
|
if !slices.Contains(p.User.channels, p.ChannelName.canonical) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNotInChannel, p.ChannelName.Value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
func(p PartAction) {
|
||||||
|
name := p.ChannelName.canonical
|
||||||
|
p.User.channels = slices.DeleteFunc(p.User.channels, func(ccn canonicalChannelName) bool {
|
||||||
|
return ccn == name
|
||||||
|
})
|
||||||
|
|
||||||
|
channelUsers := users.channelNameIndex[name]
|
||||||
|
delete(channelUsers, p.User)
|
||||||
|
if len(channelUsers) == 0 {
|
||||||
|
delete(users.channelNameIndex, name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
@@ -1,122 +0,0 @@
|
|||||||
package world
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/irc/users"
|
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
type World struct {
|
|
||||||
Server *transport.Server
|
|
||||||
UsersSystem *users.UsersSystem
|
|
||||||
}
|
|
||||||
|
|
||||||
type WrappedMessage struct {
|
|
||||||
World *World
|
|
||||||
Sender *users.User
|
|
||||||
Content transport.Content
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWorld(server *transport.Server) *World {
|
|
||||||
usersSystem := users.NewUsersSystem()
|
|
||||||
|
|
||||||
return &World{
|
|
||||||
Server: server,
|
|
||||||
UsersSystem: usersSystem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (world *World) Wrap(msg transport.IncomingMessage) WrappedMessage {
|
|
||||||
sender := world.UsersSystem.ByClientIdOrCreate(msg.Sender)
|
|
||||||
|
|
||||||
return WrappedMessage{
|
|
||||||
World: world,
|
|
||||||
Sender: sender,
|
|
||||||
Content: msg.Content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// transmission of messages
|
|
||||||
func (world *World) RelayToVagueDestination(
|
|
||||||
msg WrappedMessage,
|
|
||||||
name string,
|
|
||||||
exclude []transport.ClientId,
|
|
||||||
) {
|
|
||||||
nick, err := users.ValidateNick(name)
|
|
||||||
if err == nil {
|
|
||||||
// so it's a nick!
|
|
||||||
world.RelayToNick(msg, nick, exclude)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channel, err := users.ValidateChannelName(name)
|
|
||||||
if err == nil {
|
|
||||||
// so it's a channel!
|
|
||||||
world.RelayToChannel(msg, channel, exclude)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Fatalf("not sure how to send to %s", name)
|
|
||||||
// TODO: Error response: "what is this?"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (world *World) RelayToClient(
|
|
||||||
msg WrappedMessage,
|
|
||||||
client transport.ClientId,
|
|
||||||
exclude []transport.ClientId,
|
|
||||||
) {
|
|
||||||
content := createAnnotatedContent(msg)
|
|
||||||
if slices.Contains(exclude, client) {
|
|
||||||
return // don't relay
|
|
||||||
}
|
|
||||||
world.Server.SendMessage(client, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (world *World) RelayToNick(
|
|
||||||
msg WrappedMessage,
|
|
||||||
nick users.Nick,
|
|
||||||
exclude []transport.ClientId,
|
|
||||||
) {
|
|
||||||
content := createAnnotatedContent(msg)
|
|
||||||
|
|
||||||
user := world.UsersSystem.ByNick(nick)
|
|
||||||
if user == nil {
|
|
||||||
// TODO: Send an error reply. The user didn't exist
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(exclude, user.GetClientId()) {
|
|
||||||
return // don't relay
|
|
||||||
}
|
|
||||||
|
|
||||||
world.Server.SendMessage(user.GetClientId(), content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (world *World) RelayToChannel(
|
|
||||||
msg WrappedMessage,
|
|
||||||
channelName users.ChannelName,
|
|
||||||
exclude []transport.ClientId,
|
|
||||||
) {
|
|
||||||
content := createAnnotatedContent(msg)
|
|
||||||
|
|
||||||
members := world.UsersSystem.ByChannel(channelName)
|
|
||||||
log.Printf("Members of %s: %v\n", channelName, members)
|
|
||||||
for member := range members {
|
|
||||||
if slices.Contains(exclude, member.GetClientId()) {
|
|
||||||
return // don't relay
|
|
||||||
}
|
|
||||||
world.Server.SendMessage(member.GetClientId(), content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createAnnotatedContent(
|
|
||||||
msg WrappedMessage,
|
|
||||||
) transport.Content {
|
|
||||||
content := msg.Content
|
|
||||||
fullSource := fmt.Sprintf("%s!clients/%d", msg.Sender.GetNick().Value, msg.Sender.GetClientId())
|
|
||||||
content.Source = &fullSource
|
|
||||||
return content
|
|
||||||
}
|
|
115
src/logic/rules.go
Normal file
115
src/logic/rules.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
/// An Inform 7-style rulebook.
|
||||||
|
///
|
||||||
|
/// This is pretty icky, but basically the concept here:
|
||||||
|
///
|
||||||
|
/// For any action type, you can make a series of rules to:
|
||||||
|
///
|
||||||
|
/// (1) check if it's permissible
|
||||||
|
/// (2) actually carry it out
|
||||||
|
///
|
||||||
|
/// These rules will be fired off in the order that they were added
|
||||||
|
/// with the rule that a failed check results in an error,
|
||||||
|
/// and a successful check results in all of the carryOut steps
|
||||||
|
/// actually being caused to occur. (They are not allowed to fail.)
|
||||||
|
type Rulebook struct {
|
||||||
|
chapters []anyChapter
|
||||||
|
}
|
||||||
|
|
||||||
|
type chapter[T any] struct {
|
||||||
|
check []func(T) error
|
||||||
|
carryOut []func(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
type anyChapter interface {
|
||||||
|
dispatchOn(interface{}) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rule[T any] struct {
|
||||||
|
check *func(T) error
|
||||||
|
carryOut *func(T)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyRule interface {
|
||||||
|
addTo(rb *Rulebook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRulebook() *Rulebook {
|
||||||
|
return &Rulebook{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRule[T any](check func(T) error, carryOut func(T)) *rule[T] {
|
||||||
|
return &rule[T]{
|
||||||
|
check: &check,
|
||||||
|
carryOut: &carryOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// why is this done in the rule and not the rulebook?
|
||||||
|
// because go lacks parameterized types on methods, so we _have_ to
|
||||||
|
// flip the control flow to put this logic in the type with the most knowledge
|
||||||
|
// of the generic type
|
||||||
|
func (r *rule[T]) addTo(rb *Rulebook) {
|
||||||
|
for i, ch := range rb.chapters {
|
||||||
|
if chSpecific, ok := ch.(chapter[T]); ok {
|
||||||
|
r.addToChapter(&chSpecific)
|
||||||
|
rb.chapters[i] = chSpecific
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newChapter := chapter[T]{}
|
||||||
|
r.addToChapter(&newChapter)
|
||||||
|
rb.chapters = append(rb.chapters, newChapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rule[T]) addToChapter(chapter *chapter[T]) {
|
||||||
|
if check := r.check; check != nil {
|
||||||
|
chapter.check = append(chapter.check, *check)
|
||||||
|
}
|
||||||
|
if carryOut := r.carryOut; carryOut != nil {
|
||||||
|
chapter.carryOut = append(chapter.carryOut, *carryOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *Rulebook) Add(r AnyRule) {
|
||||||
|
r.addTo(rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *Rulebook) Perform(action interface{}) error {
|
||||||
|
for _, ch := range rb.chapters {
|
||||||
|
handled, err := ch.dispatchOn(action)
|
||||||
|
if handled {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// there were zero rules
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch chapter[T]) dispatchOn(action interface{}) (bool, error) {
|
||||||
|
if specializedAction, ok := action.(T); ok {
|
||||||
|
return true, ch.dispatchOnSpecific(specializedAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// false: wrong dispatcher
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch chapter[T]) dispatchOnSpecific(item T) error {
|
||||||
|
for _, checkRule := range ch.check {
|
||||||
|
err := checkRule(item)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, carryOutRule := range ch.carryOut {
|
||||||
|
carryOutRule(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@@ -29,7 +29,7 @@ func NewServer(address string) (*Server, error) {
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
connectedClients: newConnectedClients(),
|
connectedClients: newConnectedClients(),
|
||||||
incomingMessages: make(chan IncomingMessage),
|
incomingMessages: make(chan IncomingMessage, 1024),
|
||||||
}
|
}
|
||||||
|
|
||||||
go (func() {
|
go (func() {
|
||||||
@@ -55,7 +55,7 @@ func (server *Server) handleConnection(conn net.Conn) {
|
|||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
clientCtx, cancel := context.WithCancelCause(server.ctx)
|
clientCtx, cancel := context.WithCancelCause(server.ctx)
|
||||||
outgoingMessages := make(chan OutgoingMessage)
|
outgoingMessages := make(chan OutgoingMessage, 1024)
|
||||||
|
|
||||||
clientId := server.connectedClients.Enroll(func(id ClientId) ConnectedClient {
|
clientId := server.connectedClients.Enroll(func(id ClientId) ConnectedClient {
|
||||||
return ConnectedClient{
|
return ConnectedClient{
|
||||||
@@ -141,7 +141,9 @@ func (server *Server) SendMessage(client ClientId, content Content) {
|
|||||||
Content: content,
|
Content: content,
|
||||||
}
|
}
|
||||||
server.connectedClients.BorrowIfPresent(client, func(connectedClient *ConnectedClient) {
|
server.connectedClients.BorrowIfPresent(client, func(connectedClient *ConnectedClient) {
|
||||||
|
log.Printf("putting in outgoing")
|
||||||
connectedClient.outgoingMessages <- outgoing
|
connectedClient.outgoingMessages <- outgoing
|
||||||
|
log.Printf("done putting in outgoing")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user