Rewriting (1)

This commit is contained in:
2025-09-30 22:27:59 -07:00
parent d340bc49da
commit 5b29c26102
10 changed files with 425 additions and 246 deletions

View File

@@ -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
}

View File

@@ -1,183 +1 @@
package users 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
}

24
src/irc/v2/broadcast.go Normal file
View File

@@ -0,0 +1,24 @@
package v2
import "git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
type BroadcastGroup struct {
channels []canonicalChannelName
users []*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 Broadcast(bcg *BroadcastGroup, content transport.Content) {
panic("TODO")
}

View File

@@ -1,7 +1,8 @@
package errors package v2
import "fmt" import "fmt"
var ErrAlreadyInChannel = fmt.Errorf("already in channel") var ErrAlreadyInChannel = fmt.Errorf("already in channel")
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")

30
src/irc/v2/globals.go Normal file
View File

@@ -0,0 +1,30 @@
package v2
type Dispatcher interface {
Salient() []interface{}
}
type uninitializedDispatcher struct{}
func (u uninitializedDispatcher) Salient() []interface{} {
panic("dispatcher should have been published")
}
var GlobalDispatcher Dispatcher = uninitializedDispatcher{}
func Dispatch[T any](askPermission func(T) error, act func(T)) error {
for _, handler := range GlobalDispatcher.Salient() {
if h, ok := handler.(T); ok {
err := askPermission(h)
if err != nil {
return err
}
}
}
for _, handler := range GlobalDispatcher.Salient() {
if h, ok := handler.(T); ok {
act(h)
}
}
return nil
}

View File

@@ -0,0 +1,38 @@
package v2
type Handler interface {
AssertTypes()
}
type NickChangeHandler interface {
AskPermissionForNickChange(user *User, oldNick *Nick, newNick *Nick) error
HandleNickChange(user *User, oldNick *Nick, newNick *Nick)
}
type PartJoinHandler interface {
AskPermissionForPart(user *User, channelName ChannelName, reason *string) error
HandlePart(user *User, channelName ChannelName, reason *string)
AskPermissionForJoin(user *User, channelName ChannelName) error
HandleJoin(user *User, channelName ChannelName)
}
type ChatMode string
const (
ChatModePrivmsg ChatMode = "PRIVMSG"
ChatModeNotice = "NOTICE"
)
type ChatHandler interface {
AskPermissionForUserMessage(user *User, mode ChatMode, destination *User, message string) error
HandleUserMessage(user *User, mode ChatMode, destination *User, message string)
AskPermissionForChannelMessage(user *User, mode ChatMode, destination ChannelName, message string) error
HandleChannelMessage(user *User, mode ChatMode, destination ChannelName, message string)
}
/*
type BroadcastHandler interface {
HandleBroadcastMessage(channel string) error
}
*/

View File

@@ -0,0 +1,109 @@
package v2
import "git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
type NotificationsSystem struct {
}
func (notifications *NotificationsSystem) AssertTypes() {
var _ NickChangeHandler = notifications
var _ PartJoinHandler = notifications
var _ ChatHandler = notifications
}
func (notifications *NotificationsSystem) AskPermissionForNickChange(user *User, oldNick *Nick, newNick *Nick) error {
return nil
}
func (notifications *NotificationsSystem) HandleNickChange(user *User, oldNick *Nick, newNick *Nick) {
if newNick == nil {
return // not expressible in the IRC protocol; probably shouldn't be allowed?
}
src := user.GetSourceString()
content := transport.Content{
Source: &src,
Command: "NICK",
Arguments: []string{newNick.Value},
}
group := NewBroadcastGroup()
group.AddChannels(user.GetChannels()...)
group.AddUsers(user)
Broadcast(group, content)
}
// TODO: Always present the channel name in its _established_ notation
func (notifications *NotificationsSystem) AskPermissionForPart(user *User, channelName ChannelName, reason *string) error {
return nil
}
func (notifications *NotificationsSystem) HandlePart(user *User, channelName ChannelName, reason *string) {
src := user.GetSourceString()
var args []string
args = append(args, string(channelName.Value))
if reason != nil {
args = append(args, *reason)
}
content := transport.Content{
Source: &src,
Command: "PART",
Arguments: args,
}
group := NewBroadcastGroup()
group.AddChannels(user.GetChannels()...)
group.AddUsers(user)
Broadcast(group, content)
}
func (notifications *NotificationsSystem) AskPermissionForJoin(user *User, channelName ChannelName) error {
return nil
}
func (notifications *NotificationsSystem) HandleJoin(user *User, channelName ChannelName) {
src := user.GetSourceString()
content := transport.Content{
Source: &src,
Command: "JOIN",
Arguments: []string{string(channelName.Value)},
}
group := NewBroadcastGroup()
group.AddChannels(user.GetChannels()...)
group.AddUsers(user)
Broadcast(group, content)
}
func (notifications *NotificationsSystem) AskPermissionForUserMessage(user *User, mode ChatMode, destination *User, message string) error {
return nil
}
func (notifications *NotificationsSystem) HandleUserMessage(user *User, mode ChatMode, destination *User, message string) {
src := user.GetSourceString()
content := transport.Content{
Source: &src,
Command: string(mode),
Arguments: []string{destination.GetNick().Value, message},
}
group := NewBroadcastGroup()
group.AddUsers(destination)
Broadcast(group, content)
}
func (notifications *NotificationsSystem) AskPermissionForChannelMessage(user *User, mode ChatMode, destination canonicalChannelName, message string) error {
return nil
}
func (notifications *NotificationsSystem) HandleChannelMessage(user *User, mode ChatMode, destination canonicalChannelName, message string) {
src := user.GetSourceString()
content := transport.Content{
Source: &src,
Command: string(mode),
Arguments: []string{string(destination), message},
}
group := NewBroadcastGroup()
group.AddChannels(destination)
Broadcast(group, content)
}

View File

@@ -1,4 +1,4 @@
package users package v2
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) {

108
src/irc/v2/user.go Normal file
View File

@@ -0,0 +1,108 @@
package v2
import (
"slices"
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
)
type User struct {
clientId transport.ClientId
sourceString string
nick *Nick
username *string
realName *string
hasReceivedAuthHandshakeReply bool
channels []canonicalChannelName
}
func NewUsersSystem() *UsersSystem {
return &UsersSystem{
clientIdIndex: make(map[transport.ClientId]*User),
nickIndex: make(map[canonicalNick]*User),
channelNameIndex: make(map[canonicalChannelName]map[*User]struct{}),
}
}
func (user *User) GetClientId() transport.ClientId {
return user.clientId
}
func (user *User) GetSourceString() string {
return user.sourceString
}
func (user *User) GetNick() *Nick {
return user.nick
}
func (user *User) SetNick(newNick *Nick) error {
oldNick := user.nick
return Dispatch(
func(nch NickChangeHandler) error {
return nch.AskPermissionForNickChange(user, oldNick, newNick)
},
func(nch NickChangeHandler) {
nch.HandleNickChange(user, oldNick, 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 Dispatch(
func(pjh PartJoinHandler) error {
return pjh.AskPermissionForJoin(user, channelName)
},
func(pjh PartJoinHandler) {
pjh.HandleJoin(user, channelName)
},
)
}
func (user *User) Part(channelName ChannelName, reason *string) error {
return Dispatch(
func(pjh PartJoinHandler) error {
return pjh.AskPermissionForPart(user, channelName, reason)
},
func(pjh PartJoinHandler) {
pjh.HandlePart(user, channelName, reason)
},
)
}

113
src/irc/v2/usersSystem.go Normal file
View File

@@ -0,0 +1,113 @@
package v2
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 (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,
}
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) AssertTypes() {
// statically assert that we implement the types we believe we do
var _ NickChangeHandler = users
var _ PartJoinHandler = users
}
func (users *UsersSystem) AskPermissionForNickChange(user *User, oldNick *Nick, newNick *Nick) error {
if oldNick != nil && newNick != nil && oldNick.canonical == newNick.canonical {
// you're _always_ allowed to change your nick to the nick you currently have
return nil
}
// is this someone else's nick?
_, ok := users.nickIndex[newNick.canonical]
if ok {
return fmt.Errorf("%w: %s", ErrNickAlreadyInUse, newNick.Value)
}
return nil
}
func (users *UsersSystem) HandleNickChange(user *User, oldNick *Nick, newNick *Nick) {
if oldNick != nil {
delete(users.nickIndex, oldNick.canonical)
}
user.nick = newNick
if newNick != nil {
users.nickIndex[newNick.canonical] = user
}
}
func (users *UsersSystem) AskPermissionForPart(user *User, channelName ChannelName, reason *string) error {
if !slices.Contains(user.channels, channelName.canonical) {
return fmt.Errorf("%w: %s", ErrNotInChannel, channelName)
}
return nil
}
func (users *UsersSystem) HandlePart(user *User, channelName ChannelName, reason *string) {
name := channelName.canonical
user.channels = slices.DeleteFunc(user.channels, func(ccn canonicalChannelName) bool {
return ccn == name
})
channelUsers := users.channelNameIndex[name]
delete(channelUsers, user)
if len(channelUsers) == 0 {
delete(users.channelNameIndex, name)
}
}
func (users *UsersSystem) AskPermissionForJoin(user *User, channelName ChannelName) error {
if slices.Contains(user.channels, channelName.canonical) {
return fmt.Errorf("%w: %s", ErrAlreadyInChannel, channelName)
}
return nil
}
func (users *UsersSystem) HandleJoin(user *User, channelName ChannelName) {
name := channelName.canonical
existing, ok := users.channelNameIndex[name]
if !ok {
existing = make(map[*User]struct{})
users.channelNameIndex[name] = existing
}
existing[user] = struct{}{}
}