Institute Inform-style rulebooks
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/logic"
|
||||||
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
"git.chromaticdragon.app/pyrex/minimal-irc-server/v2/src/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Globals struct {
|
type Globals struct {
|
||||||
Server *transport.Server
|
Server *transport.Server
|
||||||
|
Rulebook *logic.Rulebook
|
||||||
Users *UsersSystem
|
Users *UsersSystem
|
||||||
Notifications *NotificationsSystem
|
Notifications *NotificationsSystem
|
||||||
}
|
}
|
||||||
@@ -15,9 +17,12 @@ var globals *Globals = nil
|
|||||||
func InitializeGlobals(server *transport.Server) {
|
func InitializeGlobals(server *transport.Server) {
|
||||||
globals = &Globals{
|
globals = &Globals{
|
||||||
Server: server,
|
Server: server,
|
||||||
|
Rulebook: logic.NewRulebook(),
|
||||||
Users: NewUsersSystem(),
|
Users: NewUsersSystem(),
|
||||||
Notifications: NewNotificationsSystem(),
|
Notifications: NewNotificationsSystem(),
|
||||||
}
|
}
|
||||||
|
globals.Users.InitializeRules()
|
||||||
|
globals.Notifications.InitializeRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGlobals() *Globals {
|
func GetGlobals() *Globals {
|
||||||
@@ -27,29 +32,10 @@ func GetGlobals() *Globals {
|
|||||||
return globals
|
return globals
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Globals) getHandlers() []Handler {
|
func AddNewRule[T any](check func(T) error, carryOut func(T)) {
|
||||||
return []Handler{
|
GetGlobals().Rulebook.Add(logic.NewRule(check, carryOut))
|
||||||
g.Users,
|
|
||||||
g.Notifications,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Dispatch[T any](askPermission func(T) error, act func(T)) error {
|
func Perform[T any](action T) error {
|
||||||
globals := GetGlobals()
|
return GetGlobals().Rulebook.Perform(action)
|
||||||
handlers := globals.getHandlers()
|
|
||||||
|
|
||||||
for _, handler := range handlers {
|
|
||||||
if h, ok := handler.(T); ok {
|
|
||||||
err := askPermission(h)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, handler := range handlers {
|
|
||||||
if h, ok := handler.(T); ok {
|
|
||||||
act(h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@@ -4,16 +4,21 @@ type Handler interface {
|
|||||||
AssertTypes()
|
AssertTypes()
|
||||||
}
|
}
|
||||||
|
|
||||||
type NickChangeHandler interface {
|
type NickChangeAction struct {
|
||||||
AskPermissionForNickChange(user *User, oldNick *Nick, newNick *Nick) error
|
User *User
|
||||||
HandleNickChange(user *User, oldNick *Nick, newNick *Nick)
|
OldNick *Nick
|
||||||
|
NewNick *Nick
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartJoinHandler interface {
|
type PartAction struct {
|
||||||
AskPermissionForPart(user *User, channelName ChannelName, reason *string) error
|
User *User
|
||||||
HandlePart(user *User, channelName ChannelName, reason *string)
|
ChannelName ChannelName
|
||||||
AskPermissionForJoin(user *User, channelName ChannelName) error
|
Reason *string
|
||||||
HandleJoin(user *User, channelName ChannelName)
|
}
|
||||||
|
|
||||||
|
type JoinAction struct {
|
||||||
|
User *User
|
||||||
|
ChannelName ChannelName
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatMode string
|
type ChatMode string
|
||||||
@@ -23,16 +28,16 @@ const (
|
|||||||
ChatModeNotice ChatMode = "NOTICE"
|
ChatModeNotice ChatMode = "NOTICE"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChatHandler interface {
|
type SendMessageToUserAction struct {
|
||||||
AskPermissionForUserMessage(user *User, mode ChatMode, destination *User, message string) error
|
User *User
|
||||||
HandleUserMessage(user *User, mode ChatMode, destination *User, message string)
|
Mode ChatMode
|
||||||
|
Destination *User
|
||||||
AskPermissionForChannelMessage(user *User, mode ChatMode, destination canonicalChannelName, message string) error
|
Message string
|
||||||
HandleChannelMessage(user *User, mode ChatMode, destination canonicalChannelName, message string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
type SendMessageToChannelAction struct {
|
||||||
type BroadcastHandler interface {
|
User *User
|
||||||
HandleBroadcastMessage(channel string) error
|
Mode ChatMode
|
||||||
|
Destination canonicalChannelName
|
||||||
|
Message string
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
@@ -179,23 +179,19 @@ func parseChannelList(arg string) []ChannelName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleChannelMessage(sender *User, mode ChatMode, destination canonicalChannelName, message string) error {
|
func handleChannelMessage(sender *User, mode ChatMode, destination canonicalChannelName, message string) error {
|
||||||
return Dispatch(
|
return Perform(SendMessageToChannelAction{
|
||||||
func(ch ChatHandler) error {
|
User: sender,
|
||||||
return ch.AskPermissionForChannelMessage(sender, mode, destination, message)
|
Mode: mode,
|
||||||
},
|
Destination: destination,
|
||||||
func(ch ChatHandler) {
|
Message: message,
|
||||||
ch.HandleChannelMessage(sender, mode, destination, message)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleUserMessage(sender *User, mode ChatMode, destination *User, message string) error {
|
func handleUserMessage(sender *User, mode ChatMode, destination *User, message string) error {
|
||||||
return Dispatch(
|
return Perform(SendMessageToUserAction{
|
||||||
func(ch ChatHandler) error {
|
User: sender,
|
||||||
return ch.AskPermissionForUserMessage(sender, mode, destination, message)
|
Mode: mode,
|
||||||
},
|
Destination: destination,
|
||||||
func(ch ChatHandler) {
|
Message: message,
|
||||||
ch.HandleUserMessage(sender, mode, destination, message)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@@ -13,82 +13,52 @@ func NewNotificationsSystem() *NotificationsSystem {
|
|||||||
return &NotificationsSystem{}
|
return &NotificationsSystem{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (notifications *NotificationsSystem) AssertTypes() {
|
func (notifications *NotificationsSystem) InitializeRules() {
|
||||||
var _ NickChangeHandler = notifications
|
// nick changes
|
||||||
var _ PartJoinHandler = notifications
|
AddNewRule(
|
||||||
var _ ChatHandler = notifications
|
func(nc NickChangeAction) error { return nil },
|
||||||
}
|
func(nc NickChangeAction) {
|
||||||
|
if nc.NewNick == nil {
|
||||||
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?
|
return // not expressible in the IRC protocol; probably shouldn't be allowed?
|
||||||
}
|
}
|
||||||
|
|
||||||
src := user.GetSourceString()
|
src := nc.User.GetSourceString()
|
||||||
content := transport.Content{
|
content := transport.Content{
|
||||||
Source: &src,
|
Source: &src,
|
||||||
Command: "NICK",
|
Command: "NICK",
|
||||||
Arguments: []string{newNick.Value},
|
Arguments: []string{nc.NewNick.Value},
|
||||||
}
|
}
|
||||||
group := NewBroadcastGroup()
|
group := NewBroadcastGroup()
|
||||||
group.AddChannels(user.GetChannels()...)
|
group.AddChannels(nc.User.GetChannels()...)
|
||||||
group.AddUsers(user)
|
group.AddUsers(nc.User)
|
||||||
Broadcast(group, content)
|
Broadcast(group, content)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: Always present the channel name in its _established_ notation
|
// joins
|
||||||
func (notifications *NotificationsSystem) AskPermissionForPart(user *User, channelName ChannelName, reason *string) error {
|
AddNewRule(
|
||||||
return nil
|
func(j JoinAction) error { return nil },
|
||||||
}
|
func(j JoinAction) {
|
||||||
|
src := j.User.GetSourceString()
|
||||||
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(channelName.canonical)
|
|
||||||
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{
|
content := transport.Content{
|
||||||
Source: &src,
|
Source: &src,
|
||||||
Command: "JOIN",
|
Command: "JOIN",
|
||||||
Arguments: []string{string(channelName.Value)},
|
Arguments: []string{string(j.ChannelName.Value)},
|
||||||
}
|
}
|
||||||
group := NewBroadcastGroup()
|
group := NewBroadcastGroup()
|
||||||
group.AddChannels(channelName.canonical)
|
group.AddChannels(j.ChannelName.canonical)
|
||||||
group.AddUsers(user)
|
group.AddUsers(j.User)
|
||||||
Broadcast(group, content)
|
Broadcast(group, content)
|
||||||
|
|
||||||
// tell the user who is here
|
// tell the user who is here
|
||||||
group = NewBroadcastGroup()
|
group = NewBroadcastGroup()
|
||||||
group.AddUsers(user)
|
group.AddUsers(j.User)
|
||||||
src2 := "server"
|
src2 := "server"
|
||||||
|
|
||||||
var nameList strings.Builder
|
var nameList strings.Builder
|
||||||
var i = 0
|
var i = 0
|
||||||
for user := range GetGlobals().Users.ByChannel(channelName) {
|
for user := range GetGlobals().Users.ByChannel(j.ChannelName) {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
nameList.WriteString(" ")
|
nameList.WriteString(" ")
|
||||||
}
|
}
|
||||||
@@ -100,56 +70,81 @@ func (notifications *NotificationsSystem) HandleJoin(user *User, channelName Cha
|
|||||||
Source: &src2,
|
Source: &src2,
|
||||||
Command: "332",
|
Command: "332",
|
||||||
Arguments: []string{
|
Arguments: []string{
|
||||||
user.nick.Value, channelName.Value, "TODO: topic!",
|
j.User.nick.Value, j.ChannelName.Value, "TODO: topic!",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Broadcast(group, transport.Content{
|
Broadcast(group, transport.Content{
|
||||||
Source: &src2,
|
Source: &src2,
|
||||||
Command: "353",
|
Command: "353",
|
||||||
Arguments: []string{
|
Arguments: []string{
|
||||||
user.nick.Value, "=", channelName.Value, nameList.String(),
|
j.User.nick.Value, "=", j.ChannelName.Value, nameList.String(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Broadcast(group, transport.Content{
|
Broadcast(group, transport.Content{
|
||||||
Source: &src2,
|
Source: &src2,
|
||||||
Command: "366",
|
Command: "366",
|
||||||
Arguments: []string{
|
Arguments: []string{
|
||||||
user.nick.Value, channelName.Value, "End of /NAMES list",
|
j.User.nick.Value, j.ChannelName.Value, "End of /NAMES list",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
func (notifications *NotificationsSystem) AskPermissionForUserMessage(user *User, mode ChatMode, destination *User, message string) error {
|
// parts
|
||||||
return nil
|
AddNewRule(
|
||||||
}
|
func(p PartAction) error { return nil },
|
||||||
|
func(p PartAction) {
|
||||||
|
src := p.User.GetSourceString()
|
||||||
|
|
||||||
func (notifications *NotificationsSystem) HandleUserMessage(user *User, mode ChatMode, destination *User, message string) {
|
var args []string
|
||||||
src := user.GetSourceString()
|
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{
|
content := transport.Content{
|
||||||
Source: &src,
|
Source: &src,
|
||||||
Command: string(mode),
|
Command: string(s.Mode),
|
||||||
Arguments: []string{destination.GetNick().Value, message},
|
Arguments: []string{s.Destination.GetNick().Value, s.Message},
|
||||||
}
|
}
|
||||||
group := NewBroadcastGroup()
|
group := NewBroadcastGroup()
|
||||||
group.AddUsers(destination)
|
group.AddUsers(s.Destination)
|
||||||
Broadcast(group, content)
|
Broadcast(group, content)
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
func (notifications *NotificationsSystem) AskPermissionForChannelMessage(user *User, mode ChatMode, destination canonicalChannelName, message string) error {
|
// messages (to channel)
|
||||||
return nil
|
AddNewRule(
|
||||||
}
|
func(s SendMessageToChannelAction) error { return nil },
|
||||||
|
func(s SendMessageToChannelAction) {
|
||||||
func (notifications *NotificationsSystem) HandleChannelMessage(user *User, mode ChatMode, destination canonicalChannelName, message string) {
|
src := s.User.GetSourceString()
|
||||||
src := user.GetSourceString()
|
|
||||||
|
|
||||||
content := transport.Content{
|
content := transport.Content{
|
||||||
Source: &src,
|
Source: &src,
|
||||||
Command: string(mode),
|
Command: string(s.Mode),
|
||||||
Arguments: []string{string(destination), message},
|
Arguments: []string{string(s.Destination), s.Message},
|
||||||
}
|
}
|
||||||
group := NewBroadcastGroup()
|
group := NewBroadcastGroup()
|
||||||
group.AddChannels(destination)
|
group.AddChannels(s.Destination)
|
||||||
group.AddSpecificallyExcludedUsers(user)
|
group.AddSpecificallyExcludedUsers(s.User)
|
||||||
Broadcast(group, content)
|
Broadcast(group, content)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -20,14 +20,6 @@ type User struct {
|
|||||||
channels []canonicalChannelName
|
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) recomputeSourceString() {
|
func (user *User) recomputeSourceString() {
|
||||||
nick := "unknown"
|
nick := "unknown"
|
||||||
if user.nick != nil {
|
if user.nick != nil {
|
||||||
@@ -56,15 +48,7 @@ func (user *User) GetNick() *Nick {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) SetNick(newNick *Nick) error {
|
func (user *User) SetNick(newNick *Nick) error {
|
||||||
oldNick := user.nick
|
return Perform(NickChangeAction{User: user, OldNick: user.nick, NewNick: newNick})
|
||||||
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 {
|
func (user *User) GetUsername() *string {
|
||||||
@@ -103,23 +87,9 @@ func (user *User) GetChannels() []canonicalChannelName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) Join(channelName ChannelName) error {
|
func (user *User) Join(channelName ChannelName) error {
|
||||||
return Dispatch(
|
return Perform(JoinAction{User: user, ChannelName: channelName})
|
||||||
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 {
|
func (user *User) Part(channelName ChannelName, reason *string) error {
|
||||||
return Dispatch(
|
return Perform(PartAction{User: user, ChannelName: channelName, Reason: reason})
|
||||||
func(pjh PartJoinHandler) error {
|
|
||||||
return pjh.AskPermissionForPart(user, channelName, reason)
|
|
||||||
},
|
|
||||||
func(pjh PartJoinHandler) {
|
|
||||||
pjh.HandlePart(user, channelName, reason)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@@ -13,6 +13,14 @@ type UsersSystem struct {
|
|||||||
channelNameIndex map[canonicalChannelName](map[*User]struct{})
|
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 {
|
func (users *UsersSystem) ByClientIdOrCreate(clientId transport.ClientId) *User {
|
||||||
existing, ok := users.clientIdIndex[clientId]
|
existing, ok := users.clientIdIndex[clientId]
|
||||||
|
|
||||||
@@ -47,73 +55,75 @@ func (users *UsersSystem) ByCanonicalChannel(channelName canonicalChannelName) m
|
|||||||
return users.channelNameIndex[channelName]
|
return users.channelNameIndex[channelName]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (users *UsersSystem) AssertTypes() {
|
func (users *UsersSystem) InitializeRules() {
|
||||||
// statically assert that we implement the types we believe we do
|
// nick changes
|
||||||
var _ NickChangeHandler = users
|
AddNewRule(
|
||||||
var _ PartJoinHandler = users
|
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 {
|
||||||
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// is this someone else's nick?
|
// is this someone else's nick?
|
||||||
_, ok := users.nickIndex[newNick.canonical]
|
_, ok := users.nickIndex[nc.NewNick.canonical]
|
||||||
if ok {
|
if ok {
|
||||||
return fmt.Errorf("%w: %s", ErrNickAlreadyInUse, newNick.Value)
|
return fmt.Errorf("%w: %s", ErrNickAlreadyInUse, nc.NewNick.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func (users *UsersSystem) HandleNickChange(user *User, oldNick *Nick, newNick *Nick) {
|
},
|
||||||
if oldNick != nil {
|
func(nc NickChangeAction) {
|
||||||
delete(users.nickIndex, oldNick.canonical)
|
if nc.OldNick != nil {
|
||||||
|
delete(users.nickIndex, nc.OldNick.canonical)
|
||||||
}
|
}
|
||||||
user.nick = newNick
|
nc.User.nick = nc.NewNick
|
||||||
user.recomputeSourceString()
|
nc.User.recomputeSourceString()
|
||||||
|
|
||||||
if newNick != nil {
|
if nc.NewNick != nil {
|
||||||
users.nickIndex[newNick.canonical] = user
|
users.nickIndex[nc.NewNick.canonical] = nc.User
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
|
||||||
func (users *UsersSystem) AskPermissionForPart(user *User, channelName ChannelName, reason *string) error {
|
// joining channel
|
||||||
if !slices.Contains(user.channels, channelName.canonical) {
|
AddNewRule(
|
||||||
return fmt.Errorf("%w: %s", ErrNotInChannel, channelName)
|
func(j JoinAction) error {
|
||||||
|
if slices.Contains(j.User.channels, j.ChannelName.canonical) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrAlreadyInChannel, j.ChannelName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
},
|
||||||
|
func(j JoinAction) {
|
||||||
func (users *UsersSystem) HandlePart(user *User, channelName ChannelName, reason *string) {
|
name := j.ChannelName.canonical
|
||||||
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]
|
existing, ok := users.channelNameIndex[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
existing = make(map[*User]struct{})
|
existing = make(map[*User]struct{})
|
||||||
users.channelNameIndex[name] = existing
|
users.channelNameIndex[name] = existing
|
||||||
}
|
}
|
||||||
existing[user] = struct{}{}
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
Reference in New Issue
Block a user