mirror of
https://github.com/gissleh/irc.git
synced 2026-10-15 16:42:53 +00:00
Added Target interface and target types
This commit is contained in:
+66
@@ -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})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,10 +784,92 @@ 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 {
|
||||||
bytes := make([]byte, 12)
|
bytes := make([]byte, 12)
|
||||||
_, err := rand.Read(bytes)
|
_, err := rand.Read(bytes)
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}{
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user