Browse Source

Added Target interface and target types

master
Gisle Aune 7 years ago
parent
commit
65e4385b71
  1. 66
      channel.go
  2. 192
      client.go
  3. 16
      event.go
  4. 31
      list/list.go
  5. 8
      list/user.go
  6. 44
      query.go
  7. 20
      status.go
  8. 9
      target.go

66
channel.go

@ -0,0 +1,66 @@
package irc
import "git.aiterp.net/gisle/irc/list"
// A Channel is a target that manages the userlist
type Channel struct {
name string
userlist list.List
}
// Kind returns "channel"
func (channel *Channel) Kind() string {
return "channel"
}
// Name gets the channel name
func (channel *Channel) Name() string {
return channel.name
}
// UserList gets the channel userlist
func (channel *Channel) UserList() list.Immutable {
return channel.userlist.Immutable()
}
// Handle handles messages routed to this channel by the client's event loop
func (channel *Channel) Handle(event *Event, client *Client) {
switch event.Name() {
case "packet.join":
{
// Support extended-join
account := ""
if accountArg := event.Arg(1); accountArg != "" && accountArg != "*" {
account = accountArg
}
channel.userlist.Insert(list.User{
Nick: event.Nick,
User: event.User,
Host: event.Host,
Account: account,
})
}
case "packet.part", "packet.quit":
{
channel.userlist.Remove(event.Nick)
}
case "packet.account":
{
newAccount := event.Arg(0)
if newAccount != "*" && newAccount != "" {
channel.userlist.Patch(event.Nick, list.UserPatch{Account: newAccount})
} else {
channel.userlist.Patch(event.Nick, list.UserPatch{ClearAccount: true})
}
}
case "packet.chghost":
{
newUser := event.Arg(0)
newHost := event.Arg(1)
channel.userlist.Patch(event.Nick, list.UserPatch{User: newUser, Host: newHost})
}
}
}

192
client.go

@ -17,7 +17,6 @@ import (
"time" "time"
"git.aiterp.net/gisle/irc/ircutil" "git.aiterp.net/gisle/irc/ircutil"
"git.aiterp.net/gisle/irc/isupport" "git.aiterp.net/gisle/irc/isupport"
) )
@ -26,12 +25,31 @@ var supportedCaps = []string{
"cap-notify", "cap-notify",
"multi-prefix", "multi-prefix",
"userhost-in-names", "userhost-in-names",
"account-notify",
"extended-join",
"chghost",
} }
// ErrNoConnection is returned if you try to do something requiring a connection, // ErrNoConnection is returned if you try to do something requiring a connection,
// but there is none. // but there is none.
var ErrNoConnection = errors.New("irc: no connection") var ErrNoConnection = errors.New("irc: no connection")
// ErrTargetAlreadyAdded is returned by Client.AddTarget if that target has already been
// added to the client.
var ErrTargetAlreadyAdded = errors.New("irc: target already added")
// ErrTargetConflict is returned by Clinet.AddTarget if there already exists a target
// matching the name and kind.
var ErrTargetConflict = errors.New("irc: target name and kind match existing target")
// ErrTargetNotFound is returned by Clinet.RemoveTarget if the target is not part of
// the client's target list
var ErrTargetNotFound = errors.New("irc: target not found")
// ErrTargetIsStatus is returned by Clinet.RemoveTarget if the target is the client's
// status target
var ErrTargetIsStatus = errors.New("irc: cannot remove status target")
// A Client is an IRC client. You need to use New to construct it // A Client is an IRC client. You need to use New to construct it
type Client struct { type Client struct {
id string id string
@ -57,6 +75,10 @@ type Client struct {
quit bool quit bool
isupport isupport.ISupport isupport isupport.ISupport
values map[string]interface{} values map[string]interface{}
status *Status
targets []Target
targteIds map[Target]string
} }
// New creates a new client. The context can be context.Background if you want manually to // New creates a new client. The context can be context.Background if you want manually to
@ -70,8 +92,12 @@ func New(ctx context.Context, config Config) *Client {
capEnabled: make(map[string]bool), capEnabled: make(map[string]bool),
capData: make(map[string]string), capData: make(map[string]string),
config: config.WithDefaults(), config: config.WithDefaults(),
targteIds: make(map[Target]string, 16),
status: &Status{},
} }
client.AddTarget(client.status)
client.ctx, client.cancel = context.WithCancel(ctx) client.ctx, client.cancel = context.WithCancel(ctx)
go client.handleEventLoop() go client.handleEventLoop()
@ -366,6 +392,77 @@ func (client *Client) Join(channels ...string) error {
return client.Sendf("JOIN %s", strings.Join(channels, ",")) return client.Sendf("JOIN %s", strings.Join(channels, ","))
} }
// Target gets a target by kind and name
func (client *Client) Target(kind string, name string) Target {
client.mutex.RLock()
defer client.mutex.RUnlock()
for _, target := range client.targets {
if target.Kind() == kind && target.Name() == name {
return target
}
}
return nil
}
// Channel is a shorthand for getting a channel target and type asserting it.
func (client *Client) Channel(name string) *Channel {
target := client.Target("channel", name)
if target == nil {
return nil
}
return target.(*Channel)
}
// AddTarget adds a target to the client, generating a unique ID for it.
func (client *Client) AddTarget(target Target) (id string, err error) {
client.mutex.Lock()
defer client.mutex.Unlock()
for i := range client.targets {
if target == client.targets[i] {
err = ErrTargetAlreadyAdded
return
} else if target.Kind() == client.targets[i].Kind() && target.Name() == client.targets[i].Name() {
err = ErrTargetConflict
return
}
}
id = generateClientID()
client.targets = append(client.targets, target)
client.targteIds[target] = id
return
}
// RemoveTarget removes a target to the client
func (client *Client) RemoveTarget(target Target) (id string, err error) {
if target == client.status {
return "", ErrTargetIsStatus
}
client.mutex.Lock()
defer client.mutex.Unlock()
for i := range client.targets {
if target == client.targets[i] {
id = client.targteIds[target]
client.targets[i] = client.targets[len(client.targets)-1]
client.targets = client.targets[:len(client.targets)-1]
delete(client.targteIds, target)
return
}
}
err = ErrTargetNotFound
return
}
func (client *Client) handleEventLoop() { func (client *Client) handleEventLoop() {
ticker := time.NewTicker(time.Second * 30) ticker := time.NewTicker(time.Second * 30)
@ -532,6 +629,15 @@ func (client *Client) handleEvent(event *Event) {
} }
} }
case "packer.nick":
{
client.handleInTargets(event.Nick, event)
if event.Nick == client.nick {
client.SetValue("nick", event.Arg(0))
}
}
// Handle ISupport // Handle ISupport
case "packet.005": case "packet.005":
{ {
@ -678,8 +784,90 @@ func (client *Client) handleEvent(event *Event) {
client.host = event.Args[2] client.host = event.Args[2]
client.mutex.Unlock() client.mutex.Unlock()
} }
// This may be relevant in channels where the client resides.
client.handleInTargets(event.Nick, event)
}
// Join/part handling
case "packet.join":
{
var channel *Channel
if event.Nick == client.nick {
channel = &Channel{name: event.Arg(0)}
client.AddTarget(channel)
} else {
channel = client.Channel(event.Arg(0))
}
event.targets = append(event.targets, channel)
if channel != nil {
channel.Handle(event, client)
}
}
case "packet.part":
{
channel := client.Channel(event.Arg(0))
if channel == nil {
break
}
channel.Handle(event, client)
if event.Nick == client.nick {
client.RemoveTarget(channel)
} else {
event.targets = append(event.targets, channel)
}
}
case "packet.quit":
{
client.handleInTargets(event.Nick, event)
}
// Account handling
case "packet.account":
{
client.handleInTargets(event.Nick, event)
}
}
}
func (client *Client) handleInTargets(nick string, event *Event) {
client.mutex.RLock()
for i := range client.targets {
switch target := client.targets[i].(type) {
case *Channel:
{
if nick != "" {
if _, ok := target.UserList().User(event.Nick); !ok {
continue
}
}
event.targets = append(event.targets, target)
target.Handle(event, client)
}
case *Query:
{
if target.user.Nick == nick {
target.Handle(event, client)
}
}
case *Status:
{
if client.nick == event.Nick {
target.Handle(event, client)
}
}
} }
} }
client.mutex.RUnlock()
} }
func generateClientID() string { func generateClientID() string {
@ -697,7 +885,7 @@ func generateClientID() string {
return result[:24] return result[:24]
} }
binary.BigEndian.PutUint32(bytes, uint32(time.Now().Unix()))
binary.BigEndian.PutUint32(bytes[4:], uint32(time.Now().Unix()))
return hex.EncodeToString(bytes) return hex.EncodeToString(bytes)
} }

16
event.go

@ -25,6 +25,8 @@ type Event struct {
cancel context.CancelFunc cancel context.CancelFunc
killed bool killed bool
hidden bool hidden bool
targets []Target
} }
// NewEvent makes a new event with Kind, Verb, Time set and Args and Tags initialized. // NewEvent makes a new event with Kind, Verb, Time set and Args and Tags initialized.
@ -102,6 +104,20 @@ func (event *Event) Hidden() bool {
return event.hidden return event.hidden
} }
// Arg gets the argument by index. The rationale behind it is that some
// servers may use it for the last argument in JOINs and such.
func (event *Event) Arg(index int) string {
if index < 0 || index > len(event.Args) {
return ""
}
if index == len(event.Args) {
return event.Text
}
return event.Args[index]
}
// MarshalJSON makes a JSON object from the event. // MarshalJSON makes a JSON object from the event.
func (event *Event) MarshalJSON() ([]byte, error) { func (event *Event) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{

31
list/list.go

@ -242,6 +242,32 @@ func (list *List) Users() []User {
return result return result
} }
// Patch allows editing a limited subset of the user's properties.
func (list *List) Patch(nick string, patch UserPatch) (ok bool) {
list.mutex.Lock()
defer list.mutex.Unlock()
for _, user := range list.users {
if strings.EqualFold(nick, user.Nick) {
if patch.Account != "" || patch.ClearAccount {
user.Account = patch.Account
}
if patch.User != "" {
user.User = patch.User
}
if patch.Host != "" {
user.Host = patch.Host
}
return true
}
}
return false
}
// SetAutoSort enables or disables automatic sorting, which by default is enabled. // SetAutoSort enables or disables automatic sorting, which by default is enabled.
// Dislabing it makes sense when doing a massive operation. Enabling it will trigger // Dislabing it makes sense when doing a massive operation. Enabling it will trigger
// a sort. // a sort.
@ -264,6 +290,11 @@ func (list *List) Clear() {
list.mutex.Unlock() list.mutex.Unlock()
} }
// Immutable gets an immutable version of the list.
func (list *List) Immutable() Immutable {
return Immutable{list: list}
}
func (list *List) sort() { func (list *List) sort() {
sort.Slice(list.users, func(i, j int) bool { sort.Slice(list.users, func(i, j int) bool {
a := list.users[i] a := list.users[i]

8
list/user.go

@ -11,6 +11,14 @@ type User struct {
PrefixedNick string `json:"prefixedNick"` PrefixedNick string `json:"prefixedNick"`
} }
// UserPatch is used in List.Patch to apply changes to a user
type UserPatch struct {
User string
Host string
Account string
ClearAccount bool
}
// HighestMode returns the highest mode. // HighestMode returns the highest mode.
func (user *User) HighestMode() rune { func (user *User) HighestMode() rune {
if len(user.Modes) == 0 { if len(user.Modes) == 0 {

44
query.go

@ -0,0 +1,44 @@
package irc
import (
"git.aiterp.net/gisle/irc/list"
)
// A Query is a target for direct messages to and from a specific nick.
type Query struct {
user list.User
}
// Kind returns "channel"
func (query *Query) Kind() string {
return "query"
}
// Name gets the query name
func (query *Query) Name() string {
return query.user.Nick
}
// Handle handles messages routed to this channel by the client's event loop
func (query *Query) Handle(event *Event, client *Client) {
switch event.Name() {
case "packet.nick":
{
query.user.Nick = event.Arg(0)
}
case "packet.account":
{
account := ""
if accountArg := event.Arg(0); accountArg != "" && accountArg != "*" {
account = accountArg
}
query.user.Account = account
}
case "packet.chghost":
{
query.user.User = event.Arg(0)
query.user.Host = event.Arg(1)
}
}
}

20
status.go

@ -0,0 +1,20 @@
package irc
// A Status contains
type Status struct {
}
// Kind returns "status"
func (status *Status) Kind() string {
return "status"
}
// Name returns "status"
func (status *Status) Name() string {
return "Status"
}
// Handle handles messages routed to this status by the client's event loop
func (status *Status) Handle(event *Event, client *Client) {
}

9
target.go

@ -0,0 +1,9 @@
package irc
// A Target is a handler for a message meant for a limited part of the client, like a channel or
// query
type Target interface {
Kind() string
Name() string
Handle(event *Event, client *Client)
}
Loading…
Cancel
Save