mirror of
https://github.com/gissleh/irc.git
synced 2026-10-15 16:02:54 +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"
|
||||
|
||||
"git.aiterp.net/gisle/irc/ircutil"
|
||||
|
||||
"git.aiterp.net/gisle/irc/isupport"
|
||||
)
|
||||
|
||||
@@ -26,12 +25,31 @@ var supportedCaps = []string{
|
||||
"cap-notify",
|
||||
"multi-prefix",
|
||||
"userhost-in-names",
|
||||
"account-notify",
|
||||
"extended-join",
|
||||
"chghost",
|
||||
}
|
||||
|
||||
// ErrNoConnection is returned if you try to do something requiring a connection,
|
||||
// but there is none.
|
||||
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
|
||||
type Client struct {
|
||||
id string
|
||||
@@ -57,6 +75,10 @@ type Client struct {
|
||||
quit bool
|
||||
isupport isupport.ISupport
|
||||
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
|
||||
@@ -70,8 +92,12 @@ func New(ctx context.Context, config Config) *Client {
|
||||
capEnabled: make(map[string]bool),
|
||||
capData: make(map[string]string),
|
||||
config: config.WithDefaults(),
|
||||
targteIds: make(map[Target]string, 16),
|
||||
status: &Status{},
|
||||
}
|
||||
|
||||
client.AddTarget(client.status)
|
||||
|
||||
client.ctx, client.cancel = context.WithCancel(ctx)
|
||||
|
||||
go client.handleEventLoop()
|
||||
@@ -366,6 +392,77 @@ func (client *Client) Join(channels ...string) error {
|
||||
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() {
|
||||
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
|
||||
case "packet.005":
|
||||
{
|
||||
@@ -678,10 +784,92 @@ func (client *Client) handleEvent(event *Event) {
|
||||
client.host = event.Args[2]
|
||||
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 {
|
||||
bytes := make([]byte, 12)
|
||||
_, err := rand.Read(bytes)
|
||||
@@ -697,7 +885,7 @@ func generateClientID() string {
|
||||
return result[:24]
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(bytes, uint32(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint32(bytes[4:], uint32(time.Now().Unix()))
|
||||
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ type Event struct {
|
||||
cancel context.CancelFunc
|
||||
killed bool
|
||||
hidden bool
|
||||
|
||||
targets []Target
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (event *Event) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
|
||||
@@ -242,6 +242,32 @@ func (list *List) Users() []User {
|
||||
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.
|
||||
// Dislabing it makes sense when doing a massive operation. Enabling it will trigger
|
||||
// a sort.
|
||||
@@ -264,6 +290,11 @@ func (list *List) Clear() {
|
||||
list.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Immutable gets an immutable version of the list.
|
||||
func (list *List) Immutable() Immutable {
|
||||
return Immutable{list: list}
|
||||
}
|
||||
|
||||
func (list *List) sort() {
|
||||
sort.Slice(list.users, func(i, j int) bool {
|
||||
a := list.users[i]
|
||||
|
||||
@@ -11,6 +11,14 @@ type User struct {
|
||||
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.
|
||||
func (user *User) HighestMode() rune {
|
||||
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