Massive cleanup - coding style, tiny bugs, typos and other errors
This commit is contained in:
5
Makefile
5
Makefile
@@ -1,13 +1,12 @@
|
|||||||
#!/usr/bin/env gmake -f
|
#!/usr/bin/env gmake -f
|
||||||
|
|
||||||
GOOPTS=CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
||||||
|
|
||||||
all: clean build
|
all: clean build
|
||||||
|
|
||||||
build:
|
build:
|
||||||
${GOOPTS} go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go \
|
go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
|
||||||
buny.go main.go
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
go clean
|
go clean
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата.
|
Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата.
|
||||||
|
|
||||||
|
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, который следует себе задать перед его
|
||||||
|
использованием - а не забаню ли я кого-то не того?
|
||||||
|
|
||||||
## Что он может?
|
## Что он может?
|
||||||
|
|
||||||
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. Шифрование работает по
|
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. (Пока что) Шифрование
|
||||||
классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде), без поддержки
|
работает по классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде),
|
||||||
механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
|
без поддержки механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
|
||||||
|
|
||||||
Имеет возможность заносить пользователей в бан-лист согласно заданным в конфиге правилам.
|
Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам.
|
||||||
|
|
||||||
## Что он не может?
|
## Что он не может?
|
||||||
|
|
||||||
@@ -16,13 +19,9 @@
|
|||||||
* Не умеет разгадывать капчу.
|
* Не умеет разгадывать капчу.
|
||||||
|
|
||||||
## Как заставить его работать?
|
## Как заставить его работать?
|
||||||
|
Технически, бот собирается гошкой 1.20 под платформу linux. Работоспособность на других платформах не тестировалась.
|
||||||
|
|
||||||
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, кторый следует себе задать еред его
|
Чтобы получить готовые бинарники, достаточно выполнить команду:
|
||||||
использоованием - а не забаню ли я кого-то не того?
|
|
||||||
|
|
||||||
Технически, бот собирается гошкой 1.20 поод платформу linux. Работоспособность на других платформах не тестировалась.
|
|
||||||
|
|
||||||
Чтобы получить готовые бинарники достаточно выполнить команду:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make
|
||||||
@@ -30,8 +29,8 @@ make
|
|||||||
|
|
||||||
в итоге получится бинарник **buny-jabber-bot**, которому нужен конфиг data/config.json для работы.
|
в итоге получится бинарник **buny-jabber-bot**, которому нужен конфиг data/config.json для работы.
|
||||||
|
|
||||||
А также понадобятся **data/blacklist.json** и **data/whitelist.json** в которых списки правил, согласно которым *точно*
|
А также понадобятся **data/blacklist.json** и **data/whitelist.json**, в которых находятся списки правил, злодеев и
|
||||||
надо банить злодеев и списки пользователей, которых *точно* не надо банить.
|
списки проверенных пользователей.
|
||||||
|
|
||||||
### Замечание
|
### Замечание
|
||||||
|
|
||||||
@@ -73,12 +72,14 @@ presence-ов тех, кто находится в комнате на моме
|
|||||||
### Можно ли почистить ban-list?
|
### Можно ли почистить ban-list?
|
||||||
|
|
||||||
Да, бывают ситуации, когда по ошибке бот может забанить кого-то не того или, если не настроен белый список вообще ни в
|
Да, бывают ситуации, когда по ошибке бот может забанить кого-то не того или, если не настроен белый список вообще ни в
|
||||||
каком виде, то будет банить всех входящих. И если вдруг произошло так, что в бан попали все админы комнаты, то разбанить
|
каком виде, то будет банить всех входящих.
|
||||||
их может либо овнер (никто же не даст овнера боту), либо тот, кто зайдёт в комнату из-под аккаунта бота и разбанить всех
|
|
||||||
от его имени.
|
|
||||||
|
|
||||||
В далёких планах есть идея релизовать очистку бан-листа при запуске бота с настройкой `"wipe_bans": true`` в
|
И если вдруг произошло так, что в бан попали все админы комнаты, то...
|
||||||
whitelist.json для канала; кроме того, есть планы такое реализовать и в качестве команды, когда у бота появятся команды.
|
|
||||||
|
Во-первых, давать овнера боту не надо ни при каких обстоятельствах.
|
||||||
|
|
||||||
|
Во-вторых, разбанить их может только овнер, для чего надо зайти в комнату из-под аккаунта бота и разбанить всех кого
|
||||||
|
надо.
|
||||||
|
|
||||||
## Дисклеймер
|
## Дисклеймер
|
||||||
|
|
||||||
@@ -86,8 +87,8 @@ whitelist.json для канала; кроме того, есть планы т
|
|||||||
|
|
||||||
## Благодарности
|
## Благодарности
|
||||||
|
|
||||||
Во-первых `сообществу`, без него не было бы чятиков.
|
Во-первых, `сообществу`, без него не было бы чятиков.
|
||||||
|
|
||||||
Во-вторых `batman46(new)` из конференций на `jabber.ru`, за наблюдения и за некоторые идеи, воплощённыые в этом боте.
|
Во-вторых, `batman46(new)` из конференций на `jabber.ru`, за наблюдения и за некоторые идеи, воплощённые в этом боте.
|
||||||
|
|
||||||
В-третьих `U2` из конференции `radio@conference.jabber.ru` за мысли, идеи и за позитивный настрой.
|
В-третьих, `U2` из конференции `radio@conference.jabber.ru` за мысли, идеи и за позитивный настрой.
|
||||||
|
|||||||
23
buny.go
23
buny.go
@@ -10,7 +10,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buny(v xmpp.Presence) error {
|
func buny(v xmpp.Presence) error { //nolint:gocognit
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
|
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
|
||||||
@@ -24,7 +24,7 @@ func buny(v xmpp.Presence) error {
|
|||||||
room := strings.SplitN(v.From, "/", 2)[0]
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
|
||||||
if room == "" {
|
if room == "" {
|
||||||
log.Infof("We got empty room in repsence event, which kinda strange: %s", spew.Sdump(v))
|
log.Infof("We got empty room field in presence event, which kinda strange: %s", spew.Sdump(v))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -58,14 +58,13 @@ func buny(v xmpp.Presence) error {
|
|||||||
for _, cRoom := range roomsConnected {
|
for _, cRoom := range roomsConnected {
|
||||||
if cRoom == room {
|
if cRoom == room {
|
||||||
for _, bEntry := range blackList.Blacklist {
|
for _, bEntry := range blackList.Blacklist {
|
||||||
|
// Обработаем правила глобального чёрного списка
|
||||||
// Обработаем правила гобального чёрного списка
|
|
||||||
if bEntry.RoomName == "" {
|
if bEntry.RoomName == "" {
|
||||||
for _, jid_regexp := range bEntry.JidRe {
|
for _, jidRegexp := range bEntry.JidRe {
|
||||||
re, err := regexp.Compile(jid_regexp)
|
re, err := regexp.Compile(jidRegexp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp)
|
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -91,7 +90,6 @@ func buny(v xmpp.Presence) error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -99,11 +97,11 @@ func buny(v xmpp.Presence) error {
|
|||||||
|
|
||||||
// Обработаем правила конкретного канала, комнаты конференции
|
// Обработаем правила конкретного канала, комнаты конференции
|
||||||
if bEntry.RoomName == room {
|
if bEntry.RoomName == room {
|
||||||
for _, jid_regexp := range bEntry.JidRe {
|
for _, jidRegexp := range bEntry.JidRe {
|
||||||
re, err := regexp.Compile(jid_regexp)
|
re, err := regexp.Compile(jidRegexp)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp)
|
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -129,7 +127,6 @@ func buny(v xmpp.Presence) error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -141,3 +138,5 @@ func buny(v xmpp.Presence) error {
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collection это sync.Map-ка, хранящая в себе произвольные итемы
|
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
|
||||||
type Collection struct {
|
type Collection struct {
|
||||||
items sync.Map
|
items sync.Map
|
||||||
close chan struct{}
|
close chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item представляет собой интерфейс для хранения произвольных данных
|
// Item представляет собой интерфейс для хранения произвольных данных.
|
||||||
type item struct {
|
type item struct {
|
||||||
data interface{}
|
data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCollection создаёт инстанс коллекции
|
// NewCollection создаёт инстанс коллекции.
|
||||||
func NewCollection() *Collection {
|
func NewCollection() *Collection {
|
||||||
c := &Collection{
|
c := &Collection{ //nolint:exhaustruct
|
||||||
close: make(chan struct{}),
|
close: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get достаёт данные по заданному ключу из коллекции
|
// (collection *Collection) Get(key interface{}) (interface{}, bool) достаёт данные по заданному ключу из коллекции.
|
||||||
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
||||||
obj, exists := collection.items.Load(key)
|
obj, exists := collection.items.Load(key)
|
||||||
|
|
||||||
@@ -37,14 +37,14 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
|||||||
return item.data, true
|
return item.data, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set сохраняет данные с заданным ключом в коллекцию
|
// (collection *Collection) Set(key interface{}, value interface{}) сохраняет данные с заданным ключом в коллекцию.
|
||||||
func (collection *Collection) Set(key interface{}, value interface{}) {
|
func (collection *Collection) Set(key interface{}, value interface{}) {
|
||||||
collection.items.Store(key, item{
|
collection.items.Store(key, item{
|
||||||
data: value,
|
data: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range применяет функцию f ко всем ключам в коллекции
|
// (collection *Collection) Range(f func(key, value interface{}) bool) применяет функцию f ко всем ключам в коллекции.
|
||||||
func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
||||||
fn := func(key, value interface{}) bool {
|
fn := func(key, value interface{}) bool {
|
||||||
item := value.(item)
|
item := value.(item)
|
||||||
@@ -55,12 +55,12 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
|||||||
collection.items.Range(fn)
|
collection.items.Range(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete удаляет ключ и значение из коллекции данных
|
// (collection *Collection) Delete(key interface{}) удаляет ключ и значение из коллекции данных.
|
||||||
func (collection *Collection) Delete(key interface{}) {
|
func (collection *Collection) Delete(key interface{}) {
|
||||||
collection.items.Delete(key)
|
collection.items.Delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close очищает и высвобождает ресурсы, занятые коллекцией
|
// (collection *Collection) Close() очищает и высвобождает ресурсы, занятые коллекцией.
|
||||||
func (collection *Collection) Close() {
|
func (collection *Collection) Close() {
|
||||||
collection.close <- struct{}{}
|
collection.close <- struct{}{}
|
||||||
collection.items = sync.Map{}
|
collection.items = sync.Map{}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseEvent(e interface{}) {
|
func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||||
lastServerActivity = time.Now().Unix()
|
lastServerActivity = time.Now().Unix()
|
||||||
|
|
||||||
switch v := e.(type) {
|
switch v := e.(type) {
|
||||||
@@ -30,7 +30,7 @@ func parseEvent(e interface{}) {
|
|||||||
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text
|
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text
|
||||||
if v.Text != "" {
|
if v.Text != "" {
|
||||||
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
|
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
|
||||||
switch v.Type { //nolint:whitespace
|
switch v.Type { //nolint:wsl,whitespace
|
||||||
|
|
||||||
// Групповой чятик
|
// Групповой чятик
|
||||||
case "groupchat":
|
case "groupchat":
|
||||||
@@ -87,16 +87,18 @@ func parseEvent(e interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var iqStruct jabberSimpleIqGetQuery
|
var (
|
||||||
var parseError bool
|
iqStruct jabberSimpleIqGetQuery
|
||||||
|
parseError bool
|
||||||
|
)
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
switch iqStruct.Xmlns { //nolint:whitespace
|
switch iqStruct.Xmlns { //nolint:wsl,whitespace
|
||||||
|
|
||||||
// Запрос номера версии приложения
|
// Запрос номера версии приложения
|
||||||
case "jabber:iq:version":
|
case "jabber:iq:version":
|
||||||
log.Infof("Got IQ get request for version from %s", v.From)
|
log.Infof("Got IQ get request for version from %s", v.From)
|
||||||
answer := "<query xmlns=\"jabber:iq:version\">"
|
answer := "<query xmlns=\"jabber:iq:version\">" //nolint:wsl
|
||||||
answer += "<name>buny-jabber-bot</name>"
|
answer += "<name>buny-jabber-bot</name>"
|
||||||
answer += "<version>1.с_чем-то</version>"
|
answer += "<version>1.с_чем-то</version>"
|
||||||
answer += "<os>линупс</os>"
|
answer += "<os>линупс</os>"
|
||||||
@@ -109,7 +111,7 @@ func parseEvent(e interface{}) {
|
|||||||
xmpp.IQTypeResult,
|
xmpp.IQTypeResult,
|
||||||
answer,
|
answer,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Errorf("Unable to send verion info to jabber server: id=%s, err=%s", id, err)
|
log.Errorf("Unable to send version info to jabber server: id=%s, err=%s", id, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,9 +188,9 @@ func parseEvent(e interface{}) {
|
|||||||
|
|
||||||
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\" "
|
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\" "
|
||||||
answer += "node=\"http://jabber.org/protocol/commands\" />"
|
answer += "node=\"http://jabber.org/protocol/commands\" />"
|
||||||
answer += "<error type=\"cancel\">"
|
answer += "<error type=\"cancel\">" //nolint:goconst
|
||||||
answer += "<service-unavailable xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\" />"
|
answer += "<service-unavailable xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\" />"
|
||||||
answer += "</error>"
|
answer += "</error>" //nolint:goconst
|
||||||
|
|
||||||
if id, err := talk.RawInformation(
|
if id, err := talk.RawInformation(
|
||||||
v.To,
|
v.To,
|
||||||
@@ -287,7 +289,7 @@ func parseEvent(e interface{}) {
|
|||||||
default:
|
default:
|
||||||
log.Info("Got unknown IQ get pubsub something request, discarding")
|
log.Info("Got unknown IQ get pubsub something request, discarding")
|
||||||
log.Info(spew.Sdump(e))
|
log.Info(spew.Sdump(e))
|
||||||
parseError = true
|
parseError = true //nolint:wsl
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
parseError = true
|
parseError = true
|
||||||
@@ -311,6 +313,7 @@ func parseEvent(e interface{}) {
|
|||||||
"",
|
"",
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err)
|
log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err)
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,7 +338,7 @@ func parseEvent(e interface{}) {
|
|||||||
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
||||||
case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||||
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
|
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
|
||||||
serverPingTimestampRx = time.Now().Unix()
|
serverPingTimestampRx = time.Now().Unix() //nolint:wsl
|
||||||
|
|
||||||
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
|
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
|
||||||
case v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
case v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||||
@@ -405,8 +408,10 @@ func parseEvent(e interface{}) {
|
|||||||
case xmpp.IQTypeError:
|
case xmpp.IQTypeError:
|
||||||
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
||||||
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
||||||
var iqPingStruct jabberIqPing
|
var (
|
||||||
var parseError = true
|
iqPingStruct jabberIqPing
|
||||||
|
parseError = true
|
||||||
|
)
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
||||||
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
||||||
@@ -429,7 +434,7 @@ func parseEvent(e interface{}) {
|
|||||||
|
|
||||||
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
|
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
|
||||||
if parseError {
|
if parseError {
|
||||||
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAccepatble
|
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
||||||
if v.To == talk.JID() {
|
if v.To == talk.JID() {
|
||||||
@@ -503,8 +508,8 @@ func parseEvent(e interface{}) {
|
|||||||
log.Errorf(spew.Sdump(v))
|
log.Errorf(spew.Sdump(v))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affilation: %s, Role: %s, JID: %s",
|
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s",
|
||||||
v.Type, v.From, v.To, v.Show, v.Status, v.Affilation, v.Role, v.JID)
|
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID)
|
||||||
|
|
||||||
// Это наш собственный Presence
|
// Это наш собственный Presence
|
||||||
if v.Show == "" && v.Status == "" {
|
if v.Show == "" && v.Status == "" {
|
||||||
@@ -544,7 +549,7 @@ func parseEvent(e interface{}) {
|
|||||||
|
|
||||||
for _, feature := range v.Features {
|
for _, feature := range v.Features {
|
||||||
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
|
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
|
||||||
mucCaps[feature] = true
|
mucCaps[feature] = true //nolint:wsl
|
||||||
}
|
}
|
||||||
|
|
||||||
mucCapsList.Set(v.From, mucCaps)
|
mucCapsList.Set(v.From, mucCaps)
|
||||||
|
|||||||
36
globals.go
36
globals.go
@@ -6,60 +6,60 @@ import (
|
|||||||
"github.com/eleksir/go-xmpp"
|
"github.com/eleksir/go-xmpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config - это у нас глобальная штука :)
|
// Config - это у нас глобальная штука :) .
|
||||||
var config myConfig
|
var config myConfig
|
||||||
|
|
||||||
// Списки тех, кого мы не баним никогда
|
// Списки тех, кого мы не баним никогда.
|
||||||
var whiteList myWhiteList
|
var whiteList myWhiteList
|
||||||
|
|
||||||
// Списки злодеев, которых мы банм
|
// Списки злодеев, которых мы баним.
|
||||||
var blackList myBlackList
|
var blackList myBlackList
|
||||||
|
|
||||||
// Ставится в true, если мы получили сигнал на выключение
|
// Ставится в true, если мы получили сигнал на выключение.
|
||||||
var shutdown = false
|
var shutdown = false
|
||||||
|
|
||||||
// Чтобы не организовывать драку за установку коннекта
|
// Чтобы не организовывать драку за установку коннекта.
|
||||||
var connecting = false
|
var connecting = false
|
||||||
|
|
||||||
// Глобальное состояние соединения
|
// Глобальное состояние соединения.
|
||||||
var isConnected = false
|
var isConnected = false
|
||||||
|
|
||||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов
|
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||||
var sigChan = make(chan os.Signal, 1)
|
var sigChan = make(chan os.Signal, 1)
|
||||||
|
|
||||||
// Основной инстанс xmpp-клиента
|
// Основной инстанс xmpp-клиента.
|
||||||
var talk *xmpp.Client
|
var talk *xmpp.Client
|
||||||
|
|
||||||
// Опции подключения к xmpp-серверу
|
// Опции подключения к xmpp-серверу.
|
||||||
var options *xmpp.Options
|
var options *xmpp.Options
|
||||||
|
|
||||||
// Список комнат, в которых находится бот
|
// Список комнат, в которых находится бот.
|
||||||
var roomsConnected []string
|
var roomsConnected []string
|
||||||
|
|
||||||
// Время последней активности, нужно для jabber:iq:last
|
// Время последней активности, нужно для jabber:iq:last.
|
||||||
var lastActivity int64
|
var lastActivity int64
|
||||||
|
|
||||||
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
|
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
|
||||||
// сервера
|
// сервера.
|
||||||
var lastServerActivity int64
|
var lastServerActivity int64
|
||||||
|
|
||||||
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
||||||
// muc-ов
|
// muc-ов.
|
||||||
var lastMucActivity = NewCollection()
|
var lastMucActivity = NewCollection()
|
||||||
|
|
||||||
// Получен ли ответ на запрос disco#info к серверу
|
// Получен ли ответ на запрос disco#info к серверу.
|
||||||
var serverCapsQueried bool
|
var serverCapsQueried bool
|
||||||
|
|
||||||
// sync.Map-ка с капабилити сервера
|
// sync.Map-ка с капабилити сервера.
|
||||||
var serverCapsList = NewCollection()
|
var serverCapsList = NewCollection()
|
||||||
|
|
||||||
// sync.Map-ка с комнатами и их capability
|
// sync.Map-ка с комнатами и их capability.
|
||||||
var mucCapsList = NewCollection()
|
var mucCapsList = NewCollection()
|
||||||
|
|
||||||
// Время, когда был отправлен c2s ping
|
// Время, когда был отправлен c2s ping.
|
||||||
var serverPingTimestampTx int64
|
var serverPingTimestampTx int64
|
||||||
|
|
||||||
// Время, когда был принят s2c pong
|
// Время, когда был принят s2c pong.
|
||||||
var serverPingTimestampRx int64
|
var serverPingTimestampRx int64
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -4,10 +4,10 @@ go 1.20
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/eleksir/go-xmpp v1.3.0
|
github.com/eleksir/go-xmpp v1.4.0
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible
|
github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -1,8 +1,8 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/eleksir/go-xmpp v1.3.0 h1:A4vonXmHJ+3W4LW+BDM24GkAT0zRMhCAMZhAqTRarLE=
|
github.com/eleksir/go-xmpp v1.4.0 h1:CJ91zeoLoV0FqhhcvZnkBjeuaXWbWgRTJ/HrzKugM4s=
|
||||||
github.com/eleksir/go-xmpp v1.3.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8=
|
github.com/eleksir/go-xmpp v1.4.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8=
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
|
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
@@ -14,8 +14,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
|||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
|
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
|
||||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
215
lib.go
215
lib.go
@@ -15,7 +15,7 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения
|
// sigHandler() Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||||
func sigHandler() {
|
func sigHandler() {
|
||||||
log.Debug("Installing signal handler")
|
log.Debug("Installing signal handler")
|
||||||
|
|
||||||
@@ -28,19 +28,19 @@ func sigHandler() {
|
|||||||
case syscall.SIGQUIT:
|
case syscall.SIGQUIT:
|
||||||
log.Infoln("Got SIGQUIT, quitting")
|
log.Infoln("Got SIGQUIT, quitting")
|
||||||
|
|
||||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
|
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true
|
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true.
|
||||||
shutdown = true
|
shutdown = true
|
||||||
|
|
||||||
if isConnected && !shutdown {
|
if isConnected && !shutdown {
|
||||||
log.Debug("Try to set our presence to Unavailable and status to Offline")
|
log.Debug("Try to set our presence to Unavailable and status to Offline")
|
||||||
|
|
||||||
// Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в offline
|
// Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в offline
|
||||||
// Пока за неимением лучшего сообщим об этом самим себе
|
// Пока за неимением лучшего сообщим об этом самим себе.
|
||||||
for _, room := range roomsConnected {
|
for _, room := range roomsConnected {
|
||||||
if _, err := talk.SendPresence(
|
if _, err := talk.SendPresence(
|
||||||
xmpp.Presence{ //nolint:exhaustruct
|
xmpp.Presence{ //nolint:exhaustruct
|
||||||
@@ -53,7 +53,7 @@ func sigHandler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// И закрываем соединение
|
// И закрываем соединение.
|
||||||
log.Debugf("Closing connection to jabber server")
|
log.Debugf("Closing connection to jabber server")
|
||||||
|
|
||||||
if err := talk.Close(); err != nil {
|
if err := talk.Close(); err != nil {
|
||||||
@@ -65,13 +65,16 @@ func sigHandler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет
|
// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
|
||||||
func readConfig() {
|
// нет.
|
||||||
|
func readConfig() { //nolint:gocognit
|
||||||
configLoaded := false
|
configLoaded := false
|
||||||
executablePath, err := os.Executable()
|
executablePath, err := os.Executable()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to get current executable path: %s", err)
|
log.Errorf("Unable to get current executable path: %s", err)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
||||||
@@ -86,12 +89,12 @@ func readConfig() {
|
|||||||
for _, location := range locations {
|
for _, location := range locations {
|
||||||
fileInfo, err := os.Stat(location)
|
fileInfo, err := os.Stat(location)
|
||||||
|
|
||||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||||
if fileInfo.Size() > 65535 {
|
if fileInfo.Size() > 65535 {
|
||||||
log.Warnf("Config file %s is too long for config, skipping", location)
|
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ func readConfig() {
|
|||||||
|
|
||||||
buf, err := os.ReadFile(location)
|
buf, err := os.ReadFile(location)
|
||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата
|
// Не удалось прочитать, попробуем следующего кандидата.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||||
|
|
||||||
@@ -110,18 +113,21 @@ func readConfig() {
|
|||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||||
var sampleConfig myConfig
|
var (
|
||||||
var tmp map[string]interface{}
|
sampleConfig myConfig
|
||||||
|
tmp map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
// Не удалось распарсить - попробуем следующего кандидата.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpjson, err := json.Marshal(tmp)
|
tmpJSON, err := json.Marshal(tmp)
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
// Не удалось преобразовать map-ку в json
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -130,7 +136,7 @@ func readConfig() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil {
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -138,20 +144,22 @@ func readConfig() {
|
|||||||
|
|
||||||
// Значения для Jabber-клиента
|
// Значения для Jabber-клиента
|
||||||
if sampleConfig.Jabber.Server == "" {
|
if sampleConfig.Jabber.Server == "" {
|
||||||
sampleConfig.Jabber.Server = "localhost"
|
log.Error("Jabber server is not defined in config, using localhost")
|
||||||
log.Errorf("Jabber server is not defined in config, using localhost")
|
sampleConfig.Jabber.Server = "localhost" //nolint:wsl
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.Port == 0 {
|
if sampleConfig.Jabber.Port == 0 {
|
||||||
sampleConfig.Jabber.Port = 5552
|
sampleConfig.Jabber.Port = 5222
|
||||||
|
|
||||||
if sampleConfig.Jabber.Ssl {
|
if sampleConfig.Jabber.Ssl {
|
||||||
if !sampleConfig.Jabber.StartTLS {
|
if !sampleConfig.Jabber.StartTLS {
|
||||||
sampleConfig.Jabber.Port = 5553
|
sampleConfig.Jabber.Port = 5223
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("Jabber port is not defined in config, using 6667")
|
log.Info("Jabber port is not defined in config, using 5223")
|
||||||
|
} else {
|
||||||
|
log.Info("Jabber port is not defined in config, using 5222")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !sampleConfig.Jabber.Ssl {
|
if !sampleConfig.Jabber.Ssl {
|
||||||
@@ -166,26 +174,38 @@ func readConfig() {
|
|||||||
|
|
||||||
if sampleConfig.Jabber.ConnectionTimeout == 0 {
|
if sampleConfig.Jabber.ConnectionTimeout == 0 {
|
||||||
sampleConfig.Jabber.ConnectionTimeout = 10
|
sampleConfig.Jabber.ConnectionTimeout = 10
|
||||||
|
|
||||||
|
log.Info("Jabber server connection_timeout not defined in config, using 10 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.ReconnectDelay == 0 {
|
if sampleConfig.Jabber.ReconnectDelay == 0 {
|
||||||
sampleConfig.Jabber.ReconnectDelay = 3
|
sampleConfig.Jabber.ReconnectDelay = 3
|
||||||
|
|
||||||
|
log.Info("Jabber server reconnect_delay not defined in config, using 3 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.ServerPingDelay == 0 {
|
if sampleConfig.Jabber.ServerPingDelay == 0 {
|
||||||
sampleConfig.Jabber.ServerPingDelay = 60
|
sampleConfig.Jabber.ServerPingDelay = 60
|
||||||
|
|
||||||
|
log.Info("Jabber server_ping_delay not defined in config, using 60 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.MucPingDelay == 0 {
|
if sampleConfig.Jabber.MucPingDelay == 0 {
|
||||||
sampleConfig.Jabber.MucPingDelay = 900
|
sampleConfig.Jabber.MucPingDelay = 900
|
||||||
|
|
||||||
|
log.Info("Jabber muc_ping_delay not defined in config, using 900 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.MucRejoinDelay == 0 {
|
if sampleConfig.Jabber.MucRejoinDelay == 0 {
|
||||||
sampleConfig.Jabber.MucRejoinDelay = 3
|
sampleConfig.Jabber.MucRejoinDelay = 3
|
||||||
|
|
||||||
|
log.Info("Jabber muc_rejoin_delay not defined in config, using 3 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.PingSplayDelay == 0 {
|
if sampleConfig.Jabber.PingSplayDelay == 0 {
|
||||||
sampleConfig.Jabber.PingSplayDelay = 3
|
sampleConfig.Jabber.PingSplayDelay = 3
|
||||||
|
|
||||||
|
log.Info("Jabber ping_splay_delay not defined in config, using 3 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.Nick == "" {
|
if sampleConfig.Jabber.Nick == "" {
|
||||||
@@ -195,10 +215,14 @@ func readConfig() {
|
|||||||
|
|
||||||
if sampleConfig.Jabber.Resource == "" {
|
if sampleConfig.Jabber.Resource == "" {
|
||||||
sampleConfig.Jabber.Resource = "buny bot"
|
sampleConfig.Jabber.Resource = "buny bot"
|
||||||
|
|
||||||
|
log.Info("Jabber resource not defined in config, using buny bot")
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Jabber.User == "" {
|
if sampleConfig.Jabber.User == "" {
|
||||||
sampleConfig.Jabber.User = fmt.Sprintf("%s@%s", sampleConfig.Jabber.Nick, sampleConfig.Jabber.Server)
|
sampleConfig.Jabber.User = fmt.Sprintf("%s@%s", sampleConfig.Jabber.Nick, sampleConfig.Jabber.Server)
|
||||||
|
|
||||||
|
log.Infof("Jabber user not defined in config, guessing, it can be %s", sampleConfig.Jabber.User)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если sampleConfig.Jabber.Password не задан, то авторизации не будет
|
// Если sampleConfig.Jabber.Password не задан, то авторизации не будет
|
||||||
@@ -212,13 +236,15 @@ func readConfig() {
|
|||||||
|
|
||||||
if sampleConfig.Loglevel == "" {
|
if sampleConfig.Loglevel == "" {
|
||||||
sampleConfig.Loglevel = "info"
|
sampleConfig.Loglevel = "info"
|
||||||
|
|
||||||
|
log.Info("loglevel not defined in config, using info")
|
||||||
}
|
}
|
||||||
|
|
||||||
// sampleConfig.Log = "" if not set
|
// sampleConfig.Log = "" if not set
|
||||||
|
|
||||||
config = sampleConfig
|
config = sampleConfig
|
||||||
configLoaded = true
|
configLoaded = true
|
||||||
log.Infof("Using %s as config file", location)
|
log.Infof("Using %s as config file", location) //nolint:wsl
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -229,13 +255,14 @@ func readConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Читает и валидирует белые списки пользователей
|
// readWhitelist() читает и валидирует белые списки пользователей.
|
||||||
func readWhitelist() {
|
func readWhitelist() {
|
||||||
whitelistLoaded := false
|
whitelistLoaded := false
|
||||||
executablePath, err := os.Executable()
|
executablePath, err := os.Executable()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to get current executable path: %s", err)
|
log.Errorf("Unable to get current executable path: %s", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
|
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
|
||||||
@@ -274,8 +301,11 @@ func readWhitelist() {
|
|||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||||
var sampleWhitelist myWhiteList
|
var (
|
||||||
var tmp map[string]interface{}
|
sampleWhitelist myWhiteList
|
||||||
|
tmp map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
// Не удалось распарсить - попробуем следующего кандидата
|
||||||
@@ -285,7 +315,7 @@ func readWhitelist() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpjson, err := json.Marshal(tmp)
|
tmpJSON, err := json.Marshal(tmp)
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
// Не удалось преобразовать map-ку в json
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -294,7 +324,7 @@ func readWhitelist() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpjson, &sampleWhitelist); err != nil {
|
if err := json.Unmarshal(tmpJSON, &sampleWhitelist); err != nil {
|
||||||
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -302,6 +332,7 @@ func readWhitelist() {
|
|||||||
|
|
||||||
whiteList = sampleWhitelist
|
whiteList = sampleWhitelist
|
||||||
whitelistLoaded = true
|
whitelistLoaded = true
|
||||||
|
|
||||||
log.Infof("Using %s as whiteList file", location)
|
log.Infof("Using %s as whiteList file", location)
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -309,17 +340,20 @@ func readWhitelist() {
|
|||||||
|
|
||||||
if !whitelistLoaded {
|
if !whitelistLoaded {
|
||||||
log.Error("Whitelist was not loaded! Refusing to start.")
|
log.Error("Whitelist was not loaded! Refusing to start.")
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Читает и валидирует чёрные списки пользователей
|
// readBlacklist() читает и валидирует чёрные списки пользователей.
|
||||||
func readBlacklist() {
|
func readBlacklist() {
|
||||||
blacklistLoaded := false
|
blacklistLoaded := false
|
||||||
executablePath, err := os.Executable()
|
executablePath, err := os.Executable()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to get current executable path: %s", err)
|
log.Errorf("Unable to get current executable path: %s", err)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
|
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
|
||||||
@@ -358,8 +392,11 @@ func readBlacklist() {
|
|||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||||
var sampleBlacklist myBlackList
|
var (
|
||||||
var tmp map[string]interface{}
|
sampleBlacklist myBlackList
|
||||||
|
tmp map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
// Не удалось распарсить - попробуем следующего кандидата
|
||||||
@@ -369,7 +406,7 @@ func readBlacklist() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpjson, err := json.Marshal(tmp)
|
tmpJSON, err := json.Marshal(tmp)
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
// Не удалось преобразовать map-ку в json
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -378,7 +415,7 @@ func readBlacklist() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpjson, &sampleBlacklist); err != nil {
|
if err := json.Unmarshal(tmpJSON, &sampleBlacklist); err != nil {
|
||||||
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -386,6 +423,7 @@ func readBlacklist() {
|
|||||||
|
|
||||||
blackList = sampleBlacklist
|
blackList = sampleBlacklist
|
||||||
blacklistLoaded = true
|
blacklistLoaded = true
|
||||||
|
|
||||||
log.Infof("Using %s as blacklist file", location)
|
log.Infof("Using %s as blacklist file", location)
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -393,10 +431,12 @@ func readBlacklist() {
|
|||||||
|
|
||||||
if !blacklistLoaded {
|
if !blacklistLoaded {
|
||||||
log.Error("Blacklist was not loaded! Refusing to start.")
|
log.Error("Blacklist was not loaded! Refusing to start.")
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// establishConnection() устанавливает соединение с jabber-сервером.
|
||||||
func establishConnection() {
|
func establishConnection() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -404,39 +444,32 @@ func establishConnection() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// проставляем глобальные переменные
|
// Проставляем глобальные переменные.
|
||||||
connecting = true
|
connecting = true
|
||||||
isConnected = false
|
isConnected = false
|
||||||
roomsConnected = make([]string, 0)
|
roomsConnected = make([]string, 0)
|
||||||
|
|
||||||
for {
|
|
||||||
talk, err = options.NewClient()
|
talk, err = options.NewClient()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
||||||
for {
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
if _, err := talk.SendKeepAlive(); err != nil {
|
||||||
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
|
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
|
||||||
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
||||||
|
|
||||||
// Понятно, что мы уже не в процессе установления соединения, а произошла ошибка
|
// Понятно, что мы уже не в процессе установления соединения, а произошла ошибка
|
||||||
connecting = false
|
connecting = false
|
||||||
os.Exit(1)
|
|
||||||
} else {
|
|
||||||
log.Info("Connected")
|
|
||||||
|
|
||||||
break
|
os.Exit(1)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info("Connected")
|
||||||
|
|
||||||
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
|
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
|
||||||
for _, room := range config.Jabber.Channels {
|
for _, room := range config.Jabber.Channels {
|
||||||
go joinMuc(room)
|
go joinMuc(room)
|
||||||
@@ -467,22 +500,7 @@ func establishConnection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reEstablishConnection() {
|
// joinMuc(room string) джойнится к конференциям/каналам/комнатам в джаббере.
|
||||||
// Серверные capabilities нам *уже* неизвестны
|
|
||||||
serverCapsQueried = false
|
|
||||||
serverCapsList = NewCollection()
|
|
||||||
mucCapsList = NewCollection()
|
|
||||||
lastMucActivity = NewCollection()
|
|
||||||
|
|
||||||
// Пока соединение не установлено, мы про пинги ничего не знаем
|
|
||||||
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен
|
|
||||||
serverPingTimestampTx = 0
|
|
||||||
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
|
||||||
connecting = false
|
|
||||||
establishConnection()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Джойнится к конференциям/каналам/комнатам в джаббере
|
|
||||||
func joinMuc(room string) {
|
func joinMuc(room string) {
|
||||||
log.Debugf("Sending disco#info from %s to %s", talk.JID(), room)
|
log.Debugf("Sending disco#info from %s to %s", talk.JID(), room)
|
||||||
|
|
||||||
@@ -492,11 +510,13 @@ func joinMuc(room string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ждём, пока muc нам вернёт список фичей
|
// Ждём, пока muc нам вернёт список фичей.
|
||||||
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
|
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
|
||||||
var myRoom interface{}
|
var (
|
||||||
var supported bool
|
myRoom interface{}
|
||||||
var exist bool
|
supported bool
|
||||||
|
exist bool
|
||||||
|
)
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
@@ -508,32 +528,46 @@ func joinMuc(room string) {
|
|||||||
if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist {
|
if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist {
|
||||||
if supported {
|
if supported {
|
||||||
break
|
break
|
||||||
} else {
|
}
|
||||||
|
|
||||||
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
|
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := talk.JoinMUCNoHistory(room, config.Jabber.Nick); err != nil {
|
if _, err := talk.JoinMUCNoHistory(room, config.Jabber.Nick); err != nil {
|
||||||
log.Errorf("Unable to join to MUC: %s", room)
|
log.Errorf("Unable to join to MUC: %s", room)
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
|
||||||
log.Infof("Joining to MUC: %s", room)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли
|
log.Infof("Joining to MUC: %s", room)
|
||||||
|
|
||||||
|
// Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли.
|
||||||
|
entered := false
|
||||||
|
|
||||||
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
|
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
if slices.Contains(roomsConnected, room) {
|
if slices.Contains(roomsConnected, room) {
|
||||||
|
entered = true
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вот теперь точно можно слать статус
|
if !entered {
|
||||||
|
log.Errorf(
|
||||||
|
"Unable to enter to MUC %s, join timeout after %d seconds (server does not return my presence for this room)",
|
||||||
|
room,
|
||||||
|
20*int(config.Jabber.ConnectionTimeout)+1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вот теперь точно можно слать статус.
|
||||||
log.Infof("Joined to MUC: %s", room)
|
log.Infof("Joined to MUC: %s", room)
|
||||||
|
|
||||||
if _, err := talk.SendPresence(
|
if _, err := talk.SendPresence(
|
||||||
@@ -547,8 +581,9 @@ func joinMuc(room string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяет живость соединения с сервером
|
// probeServerLiveness() проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
|
||||||
func probeServerLiveness() {
|
// они выкидывают клиента через некоторое время неактивности.
|
||||||
|
func probeServerLiveness() { //nolint:gocognit
|
||||||
for {
|
for {
|
||||||
if shutdown {
|
if shutdown {
|
||||||
return
|
return
|
||||||
@@ -569,8 +604,10 @@ func probeServerLiveness() {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
if serverCapsQueried { // Сервер ответил на disco#info
|
if serverCapsQueried { // Сервер ответил на disco#info
|
||||||
var value interface{}
|
var (
|
||||||
var exist bool
|
value interface{}
|
||||||
|
exist bool
|
||||||
|
)
|
||||||
|
|
||||||
value, exist = serverCapsList.Get("urn:xmpp:ping")
|
value, exist = serverCapsList.Get("urn:xmpp:ping")
|
||||||
|
|
||||||
@@ -602,18 +639,18 @@ func probeServerLiveness() {
|
|||||||
|
|
||||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
|
||||||
serverPingTimestampTx = time.Now().Unix()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverPingTimestampTx = time.Now().Unix()
|
||||||
}
|
}
|
||||||
} else { // Первая пуля пока не вылетела, отправляем
|
} else { // Первая пуля пока не вылетела, отправляем
|
||||||
log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
|
log.Debugf("Sending first c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
|
||||||
|
|
||||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
|
||||||
serverPingTimestampTx = time.Now().Unix()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverPingTimestampTx = time.Now().Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сервер не анонсировал, что умеет в c2s пинги
|
// Сервер не анонсировал, что умеет в c2s пинги
|
||||||
@@ -622,8 +659,6 @@ func probeServerLiveness() {
|
|||||||
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
if _, err := talk.SendKeepAlive(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Сервер не ответил на disco#info
|
} else { // Сервер не ответил на disco#info
|
||||||
@@ -631,28 +666,28 @@ func probeServerLiveness() {
|
|||||||
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
if _, err := talk.SendKeepAlive(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е
|
// probeMUCLiveness() Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
||||||
func probeMUCLiveness() {
|
func probeMUCLiveness() {
|
||||||
for {
|
for {
|
||||||
for _, room := range roomsConnected {
|
for _, room := range roomsConnected {
|
||||||
var exist bool
|
var (
|
||||||
var lastActivityTs interface{}
|
exist bool
|
||||||
|
lastActivityTS interface{}
|
||||||
|
)
|
||||||
|
|
||||||
// Если записи про комнату нету, то пинговать её бессмысленно.
|
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||||
if lastActivityTs, exist = lastMucActivity.Get(room); !exist {
|
if lastActivityTS, exist = lastMucActivity.Get(room); !exist {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если время последней активности в чятике не превысило
|
// Если время последней активности в чятике не превысило
|
||||||
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем
|
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем.
|
||||||
if (time.Now().Unix() - lastActivityTs.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,10 +708,10 @@ func probeMUCLiveness() {
|
|||||||
roomMap, exist = mucCapsList.Get(room)
|
roomMap, exist = mucCapsList.Get(room)
|
||||||
|
|
||||||
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
|
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
|
||||||
// Server Optimization
|
// Server Optimization.
|
||||||
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
|
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
|
||||||
go func(room string) {
|
go func(room string) {
|
||||||
// Небольшая рандомная задержка перед пингом комнаты
|
// Небольшая рандомная задержка перед пингом комнаты.
|
||||||
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||||
time.Sleep(sleepTime)
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
|
|||||||
5
main.go
5
main.go
@@ -94,8 +94,8 @@ func main() {
|
|||||||
Debug: verboseClient,
|
Debug: verboseClient,
|
||||||
Session: false,
|
Session: false,
|
||||||
Status: "xa",
|
Status: "xa",
|
||||||
StatusMessage: "echo bot is starting up",
|
StatusMessage: fmt.Sprintf("%s bot is starting up", config.Jabber.Nick),
|
||||||
DialTimeout: 10 * time.Second,
|
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
go sigHandler()
|
go sigHandler()
|
||||||
@@ -145,7 +145,6 @@ func main() {
|
|||||||
default:
|
default:
|
||||||
// Это уже что-то странное.
|
// Это уже что-то странное.
|
||||||
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
types.go
17
types.go
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Конфиг
|
// myConfig прототип структурки с конфигом.
|
||||||
type myConfig struct {
|
type myConfig struct {
|
||||||
Jabber struct {
|
Jabber struct {
|
||||||
Server string `json:"server,omitempty"`
|
Server string `json:"server,omitempty"`
|
||||||
@@ -32,15 +32,16 @@ type myConfig struct {
|
|||||||
NoEcho bool `json:"noecho,omitempty"`
|
NoEcho bool `json:"noecho,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Белый список
|
// myWhiteList прототип структурки с белым списком jid-ов.
|
||||||
type myWhiteList struct {
|
type myWhiteList struct {
|
||||||
Whitelist []struct {
|
Whitelist []struct {
|
||||||
RoomName string `json:"room_name,omitempty"`
|
RoomName string `json:"room_name,omitempty"`
|
||||||
Jid []string `json:"jid,omitempty"`
|
Jid []string `json:"jid,omitempty"`
|
||||||
WipeBans bool `json:"wipe_bans,omitempty"`
|
WipeBans bool `json:"wipe_bans,omitempty"`
|
||||||
} `json:"whitelist,omitampty"`
|
} `json:"whitelist,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// myBlackList прототип структурки с чёрным списком jid-ов.
|
||||||
type myBlackList struct {
|
type myBlackList struct {
|
||||||
Blacklist []struct {
|
Blacklist []struct {
|
||||||
RoomName string `json:"room_name,omitempty"`
|
RoomName string `json:"room_name,omitempty"`
|
||||||
@@ -48,6 +49,8 @@ type myBlackList struct {
|
|||||||
} `json:"blacklist,omitempty"`
|
} `json:"blacklist,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jabberSimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например,
|
||||||
|
// https://xmpp.org/extensions/xep-0030.html#example-18 .
|
||||||
type jabberSimpleIqGetQuery struct {
|
type jabberSimpleIqGetQuery struct {
|
||||||
XMLName xml.Name `xml:"query"`
|
XMLName xml.Name `xml:"query"`
|
||||||
Text string `xml:",chardata"`
|
Text string `xml:",chardata"`
|
||||||
@@ -55,6 +58,7 @@ type jabberSimpleIqGetQuery struct {
|
|||||||
Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items"
|
Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jabberPubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub.
|
||||||
type jabberPubsubIQGetQuery struct {
|
type jabberPubsubIQGetQuery struct {
|
||||||
XMLName xml.Name `xml:"pubsub"`
|
XMLName xml.Name `xml:"pubsub"`
|
||||||
Text string `xml:",chardata"`
|
Text string `xml:",chardata"`
|
||||||
@@ -66,6 +70,8 @@ type jabberPubsubIQGetQuery struct {
|
|||||||
} `xml:"items"`
|
} `xml:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
|
||||||
|
// https://xmpp.org/extensions/xep-0202.html
|
||||||
type jabberTimeIqGetQuery struct {
|
type jabberTimeIqGetQuery struct {
|
||||||
// <time xmlns="urn:xmpp:time"/>
|
// <time xmlns="urn:xmpp:time"/>
|
||||||
XMLName xml.Name `xml:"time"`
|
XMLName xml.Name `xml:"time"`
|
||||||
@@ -73,13 +79,16 @@ type jabberTimeIqGetQuery struct {
|
|||||||
Xmlns string `xml:"xmlns,attr"`
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jabberIqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
|
||||||
type jabberIqPing struct {
|
type jabberIqPing struct {
|
||||||
XMLName xml.Name `xml:"ping"`
|
XMLName xml.Name `xml:"ping"`
|
||||||
Text string `xml:",chardata"`
|
Text string `xml:",chardata"`
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jabberIqErrorCancelNotAccepatble struct {
|
// jabberIqErrorCancelNotAcceptable прототип структурки для разбора IQ ответов, когда сервис (сервер, клиент, etc) не
|
||||||
|
// может или не хочет принимать наш iq-запрос https://xmpp.org/extensions/xep-0099.html
|
||||||
|
type jabberIqErrorCancelNotAcceptable struct {
|
||||||
XMLName xml.Name `xml:"error"`
|
XMLName xml.Name `xml:"error"`
|
||||||
Text string `xml:",chardata"`
|
Text string `xml:",chardata"`
|
||||||
Type string `xml:"type,attr"`
|
Type string `xml:"type,attr"`
|
||||||
|
|||||||
2
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
2
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
@@ -796,7 +796,7 @@ type Presence struct {
|
|||||||
Status string
|
Status string
|
||||||
Priority string
|
Priority string
|
||||||
ID string
|
ID string
|
||||||
Affilation string
|
Affiliation string
|
||||||
Role string
|
Role string
|
||||||
JID string
|
JID string
|
||||||
}
|
}
|
||||||
|
|||||||
44
vendor/golang.org/x/exp/slices/cmp.go
generated
vendored
Normal file
44
vendor/golang.org/x/exp/slices/cmp.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2023 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package slices
|
||||||
|
|
||||||
|
import "golang.org/x/exp/constraints"
|
||||||
|
|
||||||
|
// min is a version of the predeclared function from the Go 1.21 release.
|
||||||
|
func min[T constraints.Ordered](a, b T) T {
|
||||||
|
if a < b || isNaN(a) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// max is a version of the predeclared function from the Go 1.21 release.
|
||||||
|
func max[T constraints.Ordered](a, b T) T {
|
||||||
|
if a > b || isNaN(a) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmpLess is a copy of cmp.Less from the Go 1.21 release.
|
||||||
|
func cmpLess[T constraints.Ordered](x, y T) bool {
|
||||||
|
return (isNaN(x) && !isNaN(y)) || x < y
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmpCompare is a copy of cmp.Compare from the Go 1.21 release.
|
||||||
|
func cmpCompare[T constraints.Ordered](x, y T) int {
|
||||||
|
xNaN := isNaN(x)
|
||||||
|
yNaN := isNaN(y)
|
||||||
|
if xNaN && yNaN {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if xNaN || x < y {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if yNaN || x > y {
|
||||||
|
return +1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
357
vendor/golang.org/x/exp/slices/slices.go
generated
vendored
357
vendor/golang.org/x/exp/slices/slices.go
generated
vendored
@@ -3,23 +3,20 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// Package slices defines various functions useful with slices of any type.
|
// Package slices defines various functions useful with slices of any type.
|
||||||
// Unless otherwise specified, these functions all apply to the elements
|
|
||||||
// of a slice at index 0 <= i < len(s).
|
|
||||||
//
|
|
||||||
// Note that the less function in IsSortedFunc, SortFunc, SortStableFunc requires a
|
|
||||||
// strict weak ordering (https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings),
|
|
||||||
// or the sorting may fail to sort correctly. A common case is when sorting slices of
|
|
||||||
// floating-point numbers containing NaN values.
|
|
||||||
package slices
|
package slices
|
||||||
|
|
||||||
import "golang.org/x/exp/constraints"
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
// Equal reports whether two slices are equal: the same length and all
|
// Equal reports whether two slices are equal: the same length and all
|
||||||
// elements equal. If the lengths are different, Equal returns false.
|
// elements equal. If the lengths are different, Equal returns false.
|
||||||
// Otherwise, the elements are compared in increasing index order, and the
|
// Otherwise, the elements are compared in increasing index order, and the
|
||||||
// comparison stops at the first unequal pair.
|
// comparison stops at the first unequal pair.
|
||||||
// Floating point NaNs are not considered equal.
|
// Floating point NaNs are not considered equal.
|
||||||
func Equal[E comparable](s1, s2 []E) bool {
|
func Equal[S ~[]E, E comparable](s1, s2 S) bool {
|
||||||
if len(s1) != len(s2) {
|
if len(s1) != len(s2) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -31,12 +28,12 @@ func Equal[E comparable](s1, s2 []E) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// EqualFunc reports whether two slices are equal using a comparison
|
// EqualFunc reports whether two slices are equal using an equality
|
||||||
// function on each pair of elements. If the lengths are different,
|
// function on each pair of elements. If the lengths are different,
|
||||||
// EqualFunc returns false. Otherwise, the elements are compared in
|
// EqualFunc returns false. Otherwise, the elements are compared in
|
||||||
// increasing index order, and the comparison stops at the first index
|
// increasing index order, and the comparison stops at the first index
|
||||||
// for which eq returns false.
|
// for which eq returns false.
|
||||||
func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool {
|
func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) bool) bool {
|
||||||
if len(s1) != len(s2) {
|
if len(s1) != len(s2) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -49,45 +46,37 @@ func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare compares the elements of s1 and s2.
|
// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair
|
||||||
// The elements are compared sequentially, starting at index 0,
|
// of elements. The elements are compared sequentially, starting at index 0,
|
||||||
// until one element is not equal to the other.
|
// until one element is not equal to the other.
|
||||||
// The result of comparing the first non-matching elements is returned.
|
// The result of comparing the first non-matching elements is returned.
|
||||||
// If both slices are equal until one of them ends, the shorter slice is
|
// If both slices are equal until one of them ends, the shorter slice is
|
||||||
// considered less than the longer one.
|
// considered less than the longer one.
|
||||||
// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2.
|
// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2.
|
||||||
// Comparisons involving floating point NaNs are ignored.
|
func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int {
|
||||||
func Compare[E constraints.Ordered](s1, s2 []E) int {
|
|
||||||
s2len := len(s2)
|
|
||||||
for i, v1 := range s1 {
|
for i, v1 := range s1 {
|
||||||
if i >= s2len {
|
if i >= len(s2) {
|
||||||
return +1
|
return +1
|
||||||
}
|
}
|
||||||
v2 := s2[i]
|
v2 := s2[i]
|
||||||
switch {
|
if c := cmpCompare(v1, v2); c != 0 {
|
||||||
case v1 < v2:
|
return c
|
||||||
return -1
|
|
||||||
case v1 > v2:
|
|
||||||
return +1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(s1) < s2len {
|
if len(s1) < len(s2) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompareFunc is like Compare but uses a comparison function
|
// CompareFunc is like [Compare] but uses a custom comparison function on each
|
||||||
// on each pair of elements. The elements are compared in increasing
|
// pair of elements.
|
||||||
// index order, and the comparisons stop after the first time cmp
|
|
||||||
// returns non-zero.
|
|
||||||
// The result is the first non-zero result of cmp; if cmp always
|
// The result is the first non-zero result of cmp; if cmp always
|
||||||
// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2),
|
// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2),
|
||||||
// and +1 if len(s1) > len(s2).
|
// and +1 if len(s1) > len(s2).
|
||||||
func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
|
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int {
|
||||||
s2len := len(s2)
|
|
||||||
for i, v1 := range s1 {
|
for i, v1 := range s1 {
|
||||||
if i >= s2len {
|
if i >= len(s2) {
|
||||||
return +1
|
return +1
|
||||||
}
|
}
|
||||||
v2 := s2[i]
|
v2 := s2[i]
|
||||||
@@ -95,7 +84,7 @@ func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(s1) < s2len {
|
if len(s1) < len(s2) {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
@@ -103,7 +92,7 @@ func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
|
|||||||
|
|
||||||
// Index returns the index of the first occurrence of v in s,
|
// Index returns the index of the first occurrence of v in s,
|
||||||
// or -1 if not present.
|
// or -1 if not present.
|
||||||
func Index[E comparable](s []E, v E) int {
|
func Index[S ~[]E, E comparable](s S, v E) int {
|
||||||
for i := range s {
|
for i := range s {
|
||||||
if v == s[i] {
|
if v == s[i] {
|
||||||
return i
|
return i
|
||||||
@@ -114,7 +103,7 @@ func Index[E comparable](s []E, v E) int {
|
|||||||
|
|
||||||
// IndexFunc returns the first index i satisfying f(s[i]),
|
// IndexFunc returns the first index i satisfying f(s[i]),
|
||||||
// or -1 if none do.
|
// or -1 if none do.
|
||||||
func IndexFunc[E any](s []E, f func(E) bool) int {
|
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||||
for i := range s {
|
for i := range s {
|
||||||
if f(s[i]) {
|
if f(s[i]) {
|
||||||
return i
|
return i
|
||||||
@@ -124,39 +113,104 @@ func IndexFunc[E any](s []E, f func(E) bool) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Contains reports whether v is present in s.
|
// Contains reports whether v is present in s.
|
||||||
func Contains[E comparable](s []E, v E) bool {
|
func Contains[S ~[]E, E comparable](s S, v E) bool {
|
||||||
return Index(s, v) >= 0
|
return Index(s, v) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainsFunc reports whether at least one
|
// ContainsFunc reports whether at least one
|
||||||
// element e of s satisfies f(e).
|
// element e of s satisfies f(e).
|
||||||
func ContainsFunc[E any](s []E, f func(E) bool) bool {
|
func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
|
||||||
return IndexFunc(s, f) >= 0
|
return IndexFunc(s, f) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert inserts the values v... into s at index i,
|
// Insert inserts the values v... into s at index i,
|
||||||
// returning the modified slice.
|
// returning the modified slice.
|
||||||
// In the returned slice r, r[i] == v[0].
|
// The elements at s[i:] are shifted up to make room.
|
||||||
|
// In the returned slice r, r[i] == v[0],
|
||||||
|
// and r[i+len(v)] == value originally at r[i].
|
||||||
// Insert panics if i is out of range.
|
// Insert panics if i is out of range.
|
||||||
// This function is O(len(s) + len(v)).
|
// This function is O(len(s) + len(v)).
|
||||||
func Insert[S ~[]E, E any](s S, i int, v ...E) S {
|
func Insert[S ~[]E, E any](s S, i int, v ...E) S {
|
||||||
tot := len(s) + len(v)
|
m := len(v)
|
||||||
if tot <= cap(s) {
|
if m == 0 {
|
||||||
s2 := s[:tot]
|
return s
|
||||||
copy(s2[i+len(v):], s[i:])
|
}
|
||||||
|
n := len(s)
|
||||||
|
if i == n {
|
||||||
|
return append(s, v...)
|
||||||
|
}
|
||||||
|
if n+m > cap(s) {
|
||||||
|
// Use append rather than make so that we bump the size of
|
||||||
|
// the slice up to the next storage class.
|
||||||
|
// This is what Grow does but we don't call Grow because
|
||||||
|
// that might copy the values twice.
|
||||||
|
s2 := append(s[:i], make(S, n+m-i)...)
|
||||||
copy(s2[i:], v)
|
copy(s2[i:], v)
|
||||||
|
copy(s2[i+m:], s[i:])
|
||||||
return s2
|
return s2
|
||||||
}
|
}
|
||||||
s2 := make(S, tot)
|
s = s[:n+m]
|
||||||
copy(s2, s[:i])
|
|
||||||
copy(s2[i:], v)
|
// before:
|
||||||
copy(s2[i+len(v):], s[i:])
|
// s: aaaaaaaabbbbccccccccdddd
|
||||||
return s2
|
// ^ ^ ^ ^
|
||||||
|
// i i+m n n+m
|
||||||
|
// after:
|
||||||
|
// s: aaaaaaaavvvvbbbbcccccccc
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i i+m n n+m
|
||||||
|
//
|
||||||
|
// a are the values that don't move in s.
|
||||||
|
// v are the values copied in from v.
|
||||||
|
// b and c are the values from s that are shifted up in index.
|
||||||
|
// d are the values that get overwritten, never to be seen again.
|
||||||
|
|
||||||
|
if !overlaps(v, s[i+m:]) {
|
||||||
|
// Easy case - v does not overlap either the c or d regions.
|
||||||
|
// (It might be in some of a or b, or elsewhere entirely.)
|
||||||
|
// The data we copy up doesn't write to v at all, so just do it.
|
||||||
|
|
||||||
|
copy(s[i+m:], s[i:])
|
||||||
|
|
||||||
|
// Now we have
|
||||||
|
// s: aaaaaaaabbbbbbbbcccccccc
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i i+m n n+m
|
||||||
|
// Note the b values are duplicated.
|
||||||
|
|
||||||
|
copy(s[i:], v)
|
||||||
|
|
||||||
|
// Now we have
|
||||||
|
// s: aaaaaaaavvvvbbbbcccccccc
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i i+m n n+m
|
||||||
|
// That's the result we want.
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hard case - v overlaps c or d. We can't just shift up
|
||||||
|
// the data because we'd move or clobber the values we're trying
|
||||||
|
// to insert.
|
||||||
|
// So instead, write v on top of d, then rotate.
|
||||||
|
copy(s[n:], v)
|
||||||
|
|
||||||
|
// Now we have
|
||||||
|
// s: aaaaaaaabbbbccccccccvvvv
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i i+m n n+m
|
||||||
|
|
||||||
|
rotateRight(s[i:], m)
|
||||||
|
|
||||||
|
// Now we have
|
||||||
|
// s: aaaaaaaavvvvbbbbcccccccc
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i i+m n n+m
|
||||||
|
// That's the result we want.
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the elements s[i:j] from s, returning the modified slice.
|
// Delete removes the elements s[i:j] from s, returning the modified slice.
|
||||||
// Delete panics if s[i:j] is not a valid slice of s.
|
// Delete panics if s[i:j] is not a valid slice of s.
|
||||||
// Delete modifies the contents of the slice s; it does not create a new slice.
|
|
||||||
// Delete is O(len(s)-j), so if many items must be deleted, it is better to
|
// Delete is O(len(s)-j), so if many items must be deleted, it is better to
|
||||||
// make a single call deleting them all together than to delete one at a time.
|
// make a single call deleting them all together than to delete one at a time.
|
||||||
// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those
|
// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those
|
||||||
@@ -175,39 +229,106 @@ func Delete[S ~[]E, E any](s S, i, j int) S {
|
|||||||
// zeroing those elements so that objects they reference can be garbage
|
// zeroing those elements so that objects they reference can be garbage
|
||||||
// collected.
|
// collected.
|
||||||
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
||||||
// Don't start copying elements until we find one to delete.
|
i := IndexFunc(s, del)
|
||||||
for i, v := range s {
|
if i == -1 {
|
||||||
if del(v) {
|
|
||||||
j := i
|
|
||||||
for i++; i < len(s); i++ {
|
|
||||||
v = s[i]
|
|
||||||
if !del(v) {
|
|
||||||
s[j] = v
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s[:j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
// Don't start copying elements until we find one to delete.
|
||||||
|
for j := i + 1; j < len(s); j++ {
|
||||||
|
if v := s[j]; !del(v) {
|
||||||
|
s[i] = v
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
// Replace replaces the elements s[i:j] by the given v, and returns the
|
// Replace replaces the elements s[i:j] by the given v, and returns the
|
||||||
// modified slice. Replace panics if s[i:j] is not a valid slice of s.
|
// modified slice. Replace panics if s[i:j] is not a valid slice of s.
|
||||||
func Replace[S ~[]E, E any](s S, i, j int, v ...E) S {
|
func Replace[S ~[]E, E any](s S, i, j int, v ...E) S {
|
||||||
_ = s[i:j] // verify that i:j is a valid subslice
|
_ = s[i:j] // verify that i:j is a valid subslice
|
||||||
|
|
||||||
|
if i == j {
|
||||||
|
return Insert(s, i, v...)
|
||||||
|
}
|
||||||
|
if j == len(s) {
|
||||||
|
return append(s[:i], v...)
|
||||||
|
}
|
||||||
|
|
||||||
tot := len(s[:i]) + len(v) + len(s[j:])
|
tot := len(s[:i]) + len(v) + len(s[j:])
|
||||||
if tot <= cap(s) {
|
if tot > cap(s) {
|
||||||
s2 := s[:tot]
|
// Too big to fit, allocate and copy over.
|
||||||
copy(s2[i+len(v):], s[j:])
|
s2 := append(s[:i], make(S, tot-i)...) // See Insert
|
||||||
copy(s2[i:], v)
|
copy(s2[i:], v)
|
||||||
|
copy(s2[i+len(v):], s[j:])
|
||||||
return s2
|
return s2
|
||||||
}
|
}
|
||||||
s2 := make(S, tot)
|
|
||||||
copy(s2, s[:i])
|
r := s[:tot]
|
||||||
copy(s2[i:], v)
|
|
||||||
copy(s2[i+len(v):], s[j:])
|
if i+len(v) <= j {
|
||||||
return s2
|
// Easy, as v fits in the deleted portion.
|
||||||
|
copy(r[i:], v)
|
||||||
|
if i+len(v) != j {
|
||||||
|
copy(r[i+len(v):], s[j:])
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are expanding (v is bigger than j-i).
|
||||||
|
// The situation is something like this:
|
||||||
|
// (example has i=4,j=8,len(s)=16,len(v)=6)
|
||||||
|
// s: aaaaxxxxbbbbbbbbyy
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i j len(s) tot
|
||||||
|
// a: prefix of s
|
||||||
|
// x: deleted range
|
||||||
|
// b: more of s
|
||||||
|
// y: area to expand into
|
||||||
|
|
||||||
|
if !overlaps(r[i+len(v):], v) {
|
||||||
|
// Easy, as v is not clobbered by the first copy.
|
||||||
|
copy(r[i+len(v):], s[j:])
|
||||||
|
copy(r[i:], v)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a situation where we don't have a single place to which
|
||||||
|
// we can copy v. Parts of it need to go to two different places.
|
||||||
|
// We want to copy the prefix of v into y and the suffix into x, then
|
||||||
|
// rotate |y| spots to the right.
|
||||||
|
//
|
||||||
|
// v[2:] v[:2]
|
||||||
|
// | |
|
||||||
|
// s: aaaavvvvbbbbbbbbvv
|
||||||
|
// ^ ^ ^ ^
|
||||||
|
// i j len(s) tot
|
||||||
|
//
|
||||||
|
// If either of those two destinations don't alias v, then we're good.
|
||||||
|
y := len(v) - (j - i) // length of y portion
|
||||||
|
|
||||||
|
if !overlaps(r[i:j], v) {
|
||||||
|
copy(r[i:j], v[y:])
|
||||||
|
copy(r[len(s):], v[:y])
|
||||||
|
rotateRight(r[i:], y)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
if !overlaps(r[len(s):], v) {
|
||||||
|
copy(r[len(s):], v[:y])
|
||||||
|
copy(r[i:j], v[y:])
|
||||||
|
rotateRight(r[i:], y)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we know that v overlaps both x and y.
|
||||||
|
// That means that the entirety of b is *inside* v.
|
||||||
|
// So we don't need to preserve b at all; instead we
|
||||||
|
// can copy v first, then copy the b part of v out of
|
||||||
|
// v to the right destination.
|
||||||
|
k := startIdx(v, s[j:])
|
||||||
|
copy(r[i:], v)
|
||||||
|
copy(r[i+len(v):], r[i+k:])
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone returns a copy of the slice.
|
// Clone returns a copy of the slice.
|
||||||
@@ -222,7 +343,8 @@ func Clone[S ~[]E, E any](s S) S {
|
|||||||
|
|
||||||
// Compact replaces consecutive runs of equal elements with a single copy.
|
// Compact replaces consecutive runs of equal elements with a single copy.
|
||||||
// This is like the uniq command found on Unix.
|
// This is like the uniq command found on Unix.
|
||||||
// Compact modifies the contents of the slice s; it does not create a new slice.
|
// Compact modifies the contents of the slice s and returns the modified slice,
|
||||||
|
// which may have a smaller length.
|
||||||
// When Compact discards m elements in total, it might not modify the elements
|
// When Compact discards m elements in total, it might not modify the elements
|
||||||
// s[len(s)-m:len(s)]. If those elements contain pointers you might consider
|
// s[len(s)-m:len(s)]. If those elements contain pointers you might consider
|
||||||
// zeroing those elements so that objects they reference can be garbage collected.
|
// zeroing those elements so that objects they reference can be garbage collected.
|
||||||
@@ -242,7 +364,8 @@ func Compact[S ~[]E, E comparable](s S) S {
|
|||||||
return s[:i]
|
return s[:i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompactFunc is like Compact but uses a comparison function.
|
// CompactFunc is like [Compact] but uses an equality function to compare elements.
|
||||||
|
// For runs of elements that compare equal, CompactFunc keeps the first one.
|
||||||
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S {
|
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S {
|
||||||
if len(s) < 2 {
|
if len(s) < 2 {
|
||||||
return s
|
return s
|
||||||
@@ -280,3 +403,97 @@ func Grow[S ~[]E, E any](s S, n int) S {
|
|||||||
func Clip[S ~[]E, E any](s S) S {
|
func Clip[S ~[]E, E any](s S) S {
|
||||||
return s[:len(s):len(s)]
|
return s[:len(s):len(s)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotation algorithm explanation:
|
||||||
|
//
|
||||||
|
// rotate left by 2
|
||||||
|
// start with
|
||||||
|
// 0123456789
|
||||||
|
// split up like this
|
||||||
|
// 01 234567 89
|
||||||
|
// swap first 2 and last 2
|
||||||
|
// 89 234567 01
|
||||||
|
// join first parts
|
||||||
|
// 89234567 01
|
||||||
|
// recursively rotate first left part by 2
|
||||||
|
// 23456789 01
|
||||||
|
// join at the end
|
||||||
|
// 2345678901
|
||||||
|
//
|
||||||
|
// rotate left by 8
|
||||||
|
// start with
|
||||||
|
// 0123456789
|
||||||
|
// split up like this
|
||||||
|
// 01 234567 89
|
||||||
|
// swap first 2 and last 2
|
||||||
|
// 89 234567 01
|
||||||
|
// join last parts
|
||||||
|
// 89 23456701
|
||||||
|
// recursively rotate second part left by 6
|
||||||
|
// 89 01234567
|
||||||
|
// join at the end
|
||||||
|
// 8901234567
|
||||||
|
|
||||||
|
// TODO: There are other rotate algorithms.
|
||||||
|
// This algorithm has the desirable property that it moves each element exactly twice.
|
||||||
|
// The triple-reverse algorithm is simpler and more cache friendly, but takes more writes.
|
||||||
|
// The follow-cycles algorithm can be 1-write but it is not very cache friendly.
|
||||||
|
|
||||||
|
// rotateLeft rotates b left by n spaces.
|
||||||
|
// s_final[i] = s_orig[i+r], wrapping around.
|
||||||
|
func rotateLeft[E any](s []E, r int) {
|
||||||
|
for r != 0 && r != len(s) {
|
||||||
|
if r*2 <= len(s) {
|
||||||
|
swap(s[:r], s[len(s)-r:])
|
||||||
|
s = s[:len(s)-r]
|
||||||
|
} else {
|
||||||
|
swap(s[:len(s)-r], s[r:])
|
||||||
|
s, r = s[len(s)-r:], r*2-len(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func rotateRight[E any](s []E, r int) {
|
||||||
|
rotateLeft(s, len(s)-r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swap swaps the contents of x and y. x and y must be equal length and disjoint.
|
||||||
|
func swap[E any](x, y []E) {
|
||||||
|
for i := 0; i < len(x); i++ {
|
||||||
|
x[i], y[i] = y[i], x[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// overlaps reports whether the memory ranges a[0:len(a)] and b[0:len(b)] overlap.
|
||||||
|
func overlaps[E any](a, b []E) bool {
|
||||||
|
if len(a) == 0 || len(b) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
elemSize := unsafe.Sizeof(a[0])
|
||||||
|
if elemSize == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO: use a runtime/unsafe facility once one becomes available. See issue 12445.
|
||||||
|
// Also see crypto/internal/alias/alias.go:AnyOverlap
|
||||||
|
return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) &&
|
||||||
|
uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// startIdx returns the index in haystack where the needle starts.
|
||||||
|
// prerequisite: the needle must be aliased entirely inside the haystack.
|
||||||
|
func startIdx[E any](haystack, needle []E) int {
|
||||||
|
p := &needle[0]
|
||||||
|
for i := range haystack {
|
||||||
|
if p == &haystack[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: what if the overlap is by a non-integral number of Es?
|
||||||
|
panic("needle not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse reverses the elements of the slice in place.
|
||||||
|
func Reverse[S ~[]E, E any](s S) {
|
||||||
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
s[i], s[j] = s[j], s[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
115
vendor/golang.org/x/exp/slices/sort.go
generated
vendored
115
vendor/golang.org/x/exp/slices/sort.go
generated
vendored
@@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -exp
|
||||||
|
|
||||||
package slices
|
package slices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,57 +13,116 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Sort sorts a slice of any ordered type in ascending order.
|
// Sort sorts a slice of any ordered type in ascending order.
|
||||||
// Sort may fail to sort correctly when sorting slices of floating-point
|
// When sorting floating-point numbers, NaNs are ordered before other values.
|
||||||
// numbers containing Not-a-number (NaN) values.
|
func Sort[S ~[]E, E constraints.Ordered](x S) {
|
||||||
// Use slices.SortFunc(x, func(a, b float64) bool {return a < b || (math.IsNaN(a) && !math.IsNaN(b))})
|
|
||||||
// instead if the input may contain NaNs.
|
|
||||||
func Sort[E constraints.Ordered](x []E) {
|
|
||||||
n := len(x)
|
n := len(x)
|
||||||
pdqsortOrdered(x, 0, n, bits.Len(uint(n)))
|
pdqsortOrdered(x, 0, n, bits.Len(uint(n)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortFunc sorts the slice x in ascending order as determined by the less function.
|
// SortFunc sorts the slice x in ascending order as determined by the cmp
|
||||||
// This sort is not guaranteed to be stable.
|
// function. This sort is not guaranteed to be stable.
|
||||||
|
// cmp(a, b) should return a negative number when a < b, a positive number when
|
||||||
|
// a > b and zero when a == b.
|
||||||
//
|
//
|
||||||
// SortFunc requires that less is a strict weak ordering.
|
// SortFunc requires that cmp is a strict weak ordering.
|
||||||
// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings.
|
// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings.
|
||||||
func SortFunc[E any](x []E, less func(a, b E) bool) {
|
func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
||||||
n := len(x)
|
n := len(x)
|
||||||
pdqsortLessFunc(x, 0, n, bits.Len(uint(n)), less)
|
pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortStableFunc sorts the slice x while keeping the original order of equal
|
// SortStableFunc sorts the slice x while keeping the original order of equal
|
||||||
// elements, using less to compare elements.
|
// elements, using cmp to compare elements in the same way as [SortFunc].
|
||||||
func SortStableFunc[E any](x []E, less func(a, b E) bool) {
|
func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
||||||
stableLessFunc(x, len(x), less)
|
stableCmpFunc(x, len(x), cmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSorted reports whether x is sorted in ascending order.
|
// IsSorted reports whether x is sorted in ascending order.
|
||||||
func IsSorted[E constraints.Ordered](x []E) bool {
|
func IsSorted[S ~[]E, E constraints.Ordered](x S) bool {
|
||||||
for i := len(x) - 1; i > 0; i-- {
|
for i := len(x) - 1; i > 0; i-- {
|
||||||
if x[i] < x[i-1] {
|
if cmpLess(x[i], x[i-1]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSortedFunc reports whether x is sorted in ascending order, with less as the
|
// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the
|
||||||
// comparison function.
|
// comparison function as defined by [SortFunc].
|
||||||
func IsSortedFunc[E any](x []E, less func(a, b E) bool) bool {
|
func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool {
|
||||||
for i := len(x) - 1; i > 0; i-- {
|
for i := len(x) - 1; i > 0; i-- {
|
||||||
if less(x[i], x[i-1]) {
|
if cmp(x[i], x[i-1]) < 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Min returns the minimal value in x. It panics if x is empty.
|
||||||
|
// For floating-point numbers, Min propagates NaNs (any NaN value in x
|
||||||
|
// forces the output to be NaN).
|
||||||
|
func Min[S ~[]E, E constraints.Ordered](x S) E {
|
||||||
|
if len(x) < 1 {
|
||||||
|
panic("slices.Min: empty list")
|
||||||
|
}
|
||||||
|
m := x[0]
|
||||||
|
for i := 1; i < len(x); i++ {
|
||||||
|
m = min(m, x[i])
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinFunc returns the minimal value in x, using cmp to compare elements.
|
||||||
|
// It panics if x is empty. If there is more than one minimal element
|
||||||
|
// according to the cmp function, MinFunc returns the first one.
|
||||||
|
func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
|
||||||
|
if len(x) < 1 {
|
||||||
|
panic("slices.MinFunc: empty list")
|
||||||
|
}
|
||||||
|
m := x[0]
|
||||||
|
for i := 1; i < len(x); i++ {
|
||||||
|
if cmp(x[i], m) < 0 {
|
||||||
|
m = x[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the maximal value in x. It panics if x is empty.
|
||||||
|
// For floating-point E, Max propagates NaNs (any NaN value in x
|
||||||
|
// forces the output to be NaN).
|
||||||
|
func Max[S ~[]E, E constraints.Ordered](x S) E {
|
||||||
|
if len(x) < 1 {
|
||||||
|
panic("slices.Max: empty list")
|
||||||
|
}
|
||||||
|
m := x[0]
|
||||||
|
for i := 1; i < len(x); i++ {
|
||||||
|
m = max(m, x[i])
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxFunc returns the maximal value in x, using cmp to compare elements.
|
||||||
|
// It panics if x is empty. If there is more than one maximal element
|
||||||
|
// according to the cmp function, MaxFunc returns the first one.
|
||||||
|
func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
|
||||||
|
if len(x) < 1 {
|
||||||
|
panic("slices.MaxFunc: empty list")
|
||||||
|
}
|
||||||
|
m := x[0]
|
||||||
|
for i := 1; i < len(x); i++ {
|
||||||
|
if cmp(x[i], m) > 0 {
|
||||||
|
m = x[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
// BinarySearch searches for target in a sorted slice and returns the position
|
// BinarySearch searches for target in a sorted slice and returns the position
|
||||||
// where target is found, or the position where target would appear in the
|
// where target is found, or the position where target would appear in the
|
||||||
// sort order; it also returns a bool saying whether the target is really found
|
// sort order; it also returns a bool saying whether the target is really found
|
||||||
// in the slice. The slice must be sorted in increasing order.
|
// in the slice. The slice must be sorted in increasing order.
|
||||||
func BinarySearch[E constraints.Ordered](x []E, target E) (int, bool) {
|
func BinarySearch[S ~[]E, E constraints.Ordered](x S, target E) (int, bool) {
|
||||||
// Inlining is faster than calling BinarySearchFunc with a lambda.
|
// Inlining is faster than calling BinarySearchFunc with a lambda.
|
||||||
n := len(x)
|
n := len(x)
|
||||||
// Define x[-1] < target and x[n] >= target.
|
// Define x[-1] < target and x[n] >= target.
|
||||||
@@ -70,24 +131,24 @@ func BinarySearch[E constraints.Ordered](x []E, target E) (int, bool) {
|
|||||||
for i < j {
|
for i < j {
|
||||||
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
||||||
// i ≤ h < j
|
// i ≤ h < j
|
||||||
if x[h] < target {
|
if cmpLess(x[h], target) {
|
||||||
i = h + 1 // preserves x[i-1] < target
|
i = h + 1 // preserves x[i-1] < target
|
||||||
} else {
|
} else {
|
||||||
j = h // preserves x[j] >= target
|
j = h // preserves x[j] >= target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// i == j, x[i-1] < target, and x[j] (= x[i]) >= target => answer is i.
|
// i == j, x[i-1] < target, and x[j] (= x[i]) >= target => answer is i.
|
||||||
return i, i < n && x[i] == target
|
return i, i < n && (x[i] == target || (isNaN(x[i]) && isNaN(target)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// BinarySearchFunc works like BinarySearch, but uses a custom comparison
|
// BinarySearchFunc works like [BinarySearch], but uses a custom comparison
|
||||||
// function. The slice must be sorted in increasing order, where "increasing"
|
// function. The slice must be sorted in increasing order, where "increasing"
|
||||||
// is defined by cmp. cmp should return 0 if the slice element matches
|
// is defined by cmp. cmp should return 0 if the slice element matches
|
||||||
// the target, a negative number if the slice element precedes the target,
|
// the target, a negative number if the slice element precedes the target,
|
||||||
// or a positive number if the slice element follows the target.
|
// or a positive number if the slice element follows the target.
|
||||||
// cmp must implement the same ordering as the slice, such that if
|
// cmp must implement the same ordering as the slice, such that if
|
||||||
// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice.
|
// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice.
|
||||||
func BinarySearchFunc[E, T any](x []E, target T, cmp func(E, T) int) (int, bool) {
|
func BinarySearchFunc[S ~[]E, E, T any](x S, target T, cmp func(E, T) int) (int, bool) {
|
||||||
n := len(x)
|
n := len(x)
|
||||||
// Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 .
|
// Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 .
|
||||||
// Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0.
|
// Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0.
|
||||||
@@ -126,3 +187,9 @@ func (r *xorshift) Next() uint64 {
|
|||||||
func nextPowerOfTwo(length int) uint {
|
func nextPowerOfTwo(length int) uint {
|
||||||
return 1 << bits.Len(uint(length))
|
return 1 << bits.Len(uint(length))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isNaN reports whether x is a NaN without requiring the math package.
|
||||||
|
// This will always return false if T is not floating-point.
|
||||||
|
func isNaN[T constraints.Ordered](x T) bool {
|
||||||
|
return x != x
|
||||||
|
}
|
||||||
|
|||||||
154
vendor/golang.org/x/exp/slices/zsortfunc.go → vendor/golang.org/x/exp/slices/zsortanyfunc.go
generated
vendored
154
vendor/golang.org/x/exp/slices/zsortfunc.go → vendor/golang.org/x/exp/slices/zsortanyfunc.go
generated
vendored
@@ -6,28 +6,28 @@
|
|||||||
|
|
||||||
package slices
|
package slices
|
||||||
|
|
||||||
// insertionSortLessFunc sorts data[a:b] using insertion sort.
|
// insertionSortCmpFunc sorts data[a:b] using insertion sort.
|
||||||
func insertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
||||||
for i := a + 1; i < b; i++ {
|
for i := a + 1; i < b; i++ {
|
||||||
for j := i; j > a && less(data[j], data[j-1]); j-- {
|
for j := i; j > a && (cmp(data[j], data[j-1]) < 0); j-- {
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
data[j], data[j-1] = data[j-1], data[j]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// siftDownLessFunc implements the heap property on data[lo:hi].
|
// siftDownCmpFunc implements the heap property on data[lo:hi].
|
||||||
// first is an offset into the array where the root of the heap lies.
|
// first is an offset into the array where the root of the heap lies.
|
||||||
func siftDownLessFunc[E any](data []E, lo, hi, first int, less func(a, b E) bool) {
|
func siftDownCmpFunc[E any](data []E, lo, hi, first int, cmp func(a, b E) int) {
|
||||||
root := lo
|
root := lo
|
||||||
for {
|
for {
|
||||||
child := 2*root + 1
|
child := 2*root + 1
|
||||||
if child >= hi {
|
if child >= hi {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if child+1 < hi && less(data[first+child], data[first+child+1]) {
|
if child+1 < hi && (cmp(data[first+child], data[first+child+1]) < 0) {
|
||||||
child++
|
child++
|
||||||
}
|
}
|
||||||
if !less(data[first+root], data[first+child]) {
|
if !(cmp(data[first+root], data[first+child]) < 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data[first+root], data[first+child] = data[first+child], data[first+root]
|
data[first+root], data[first+child] = data[first+child], data[first+root]
|
||||||
@@ -35,30 +35,30 @@ func siftDownLessFunc[E any](data []E, lo, hi, first int, less func(a, b E) bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func heapSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
func heapSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
||||||
first := a
|
first := a
|
||||||
lo := 0
|
lo := 0
|
||||||
hi := b - a
|
hi := b - a
|
||||||
|
|
||||||
// Build heap with greatest element at top.
|
// Build heap with greatest element at top.
|
||||||
for i := (hi - 1) / 2; i >= 0; i-- {
|
for i := (hi - 1) / 2; i >= 0; i-- {
|
||||||
siftDownLessFunc(data, i, hi, first, less)
|
siftDownCmpFunc(data, i, hi, first, cmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop elements, largest first, into end of data.
|
// Pop elements, largest first, into end of data.
|
||||||
for i := hi - 1; i >= 0; i-- {
|
for i := hi - 1; i >= 0; i-- {
|
||||||
data[first], data[first+i] = data[first+i], data[first]
|
data[first], data[first+i] = data[first+i], data[first]
|
||||||
siftDownLessFunc(data, lo, i, first, less)
|
siftDownCmpFunc(data, lo, i, first, cmp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pdqsortLessFunc sorts data[a:b].
|
// pdqsortCmpFunc sorts data[a:b].
|
||||||
// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort.
|
// The algorithm based on pattern-defeating quicksort(pdqsort), but without the optimizations from BlockQuicksort.
|
||||||
// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf
|
// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf
|
||||||
// C++ implementation: https://github.com/orlp/pdqsort
|
// C++ implementation: https://github.com/orlp/pdqsort
|
||||||
// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/
|
// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/
|
||||||
// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort.
|
// limit is the number of allowed bad (very unbalanced) pivots before falling back to heapsort.
|
||||||
func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
|
func pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) {
|
||||||
const maxInsertion = 12
|
const maxInsertion = 12
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -70,25 +70,25 @@ func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
|
|||||||
length := b - a
|
length := b - a
|
||||||
|
|
||||||
if length <= maxInsertion {
|
if length <= maxInsertion {
|
||||||
insertionSortLessFunc(data, a, b, less)
|
insertionSortCmpFunc(data, a, b, cmp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to heapsort if too many bad choices were made.
|
// Fall back to heapsort if too many bad choices were made.
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
heapSortLessFunc(data, a, b, less)
|
heapSortCmpFunc(data, a, b, cmp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the last partitioning was imbalanced, we need to breaking patterns.
|
// If the last partitioning was imbalanced, we need to breaking patterns.
|
||||||
if !wasBalanced {
|
if !wasBalanced {
|
||||||
breakPatternsLessFunc(data, a, b, less)
|
breakPatternsCmpFunc(data, a, b, cmp)
|
||||||
limit--
|
limit--
|
||||||
}
|
}
|
||||||
|
|
||||||
pivot, hint := choosePivotLessFunc(data, a, b, less)
|
pivot, hint := choosePivotCmpFunc(data, a, b, cmp)
|
||||||
if hint == decreasingHint {
|
if hint == decreasingHint {
|
||||||
reverseRangeLessFunc(data, a, b, less)
|
reverseRangeCmpFunc(data, a, b, cmp)
|
||||||
// The chosen pivot was pivot-a elements after the start of the array.
|
// The chosen pivot was pivot-a elements after the start of the array.
|
||||||
// After reversing it is pivot-a elements before the end of the array.
|
// After reversing it is pivot-a elements before the end of the array.
|
||||||
// The idea came from Rust's implementation.
|
// The idea came from Rust's implementation.
|
||||||
@@ -98,48 +98,48 @@ func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
|
|||||||
|
|
||||||
// The slice is likely already sorted.
|
// The slice is likely already sorted.
|
||||||
if wasBalanced && wasPartitioned && hint == increasingHint {
|
if wasBalanced && wasPartitioned && hint == increasingHint {
|
||||||
if partialInsertionSortLessFunc(data, a, b, less) {
|
if partialInsertionSortCmpFunc(data, a, b, cmp) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Probably the slice contains many duplicate elements, partition the slice into
|
// Probably the slice contains many duplicate elements, partition the slice into
|
||||||
// elements equal to and elements greater than the pivot.
|
// elements equal to and elements greater than the pivot.
|
||||||
if a > 0 && !less(data[a-1], data[pivot]) {
|
if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) {
|
||||||
mid := partitionEqualLessFunc(data, a, b, pivot, less)
|
mid := partitionEqualCmpFunc(data, a, b, pivot, cmp)
|
||||||
a = mid
|
a = mid
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
mid, alreadyPartitioned := partitionLessFunc(data, a, b, pivot, less)
|
mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp)
|
||||||
wasPartitioned = alreadyPartitioned
|
wasPartitioned = alreadyPartitioned
|
||||||
|
|
||||||
leftLen, rightLen := mid-a, b-mid
|
leftLen, rightLen := mid-a, b-mid
|
||||||
balanceThreshold := length / 8
|
balanceThreshold := length / 8
|
||||||
if leftLen < rightLen {
|
if leftLen < rightLen {
|
||||||
wasBalanced = leftLen >= balanceThreshold
|
wasBalanced = leftLen >= balanceThreshold
|
||||||
pdqsortLessFunc(data, a, mid, limit, less)
|
pdqsortCmpFunc(data, a, mid, limit, cmp)
|
||||||
a = mid + 1
|
a = mid + 1
|
||||||
} else {
|
} else {
|
||||||
wasBalanced = rightLen >= balanceThreshold
|
wasBalanced = rightLen >= balanceThreshold
|
||||||
pdqsortLessFunc(data, mid+1, b, limit, less)
|
pdqsortCmpFunc(data, mid+1, b, limit, cmp)
|
||||||
b = mid
|
b = mid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// partitionLessFunc does one quicksort partition.
|
// partitionCmpFunc does one quicksort partition.
|
||||||
// Let p = data[pivot]
|
// Let p = data[pivot]
|
||||||
// Moves elements in data[a:b] around, so that data[i]<p and data[j]>=p for i<newpivot and j>newpivot.
|
// Moves elements in data[a:b] around, so that data[i]<p and data[j]>=p for i<newpivot and j>newpivot.
|
||||||
// On return, data[newpivot] = p
|
// On return, data[newpivot] = p
|
||||||
func partitionLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool) (newpivot int, alreadyPartitioned bool) {
|
func partitionCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int, alreadyPartitioned bool) {
|
||||||
data[a], data[pivot] = data[pivot], data[a]
|
data[a], data[pivot] = data[pivot], data[a]
|
||||||
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
||||||
|
|
||||||
for i <= j && less(data[i], data[a]) {
|
for i <= j && (cmp(data[i], data[a]) < 0) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
for i <= j && !less(data[j], data[a]) {
|
for i <= j && !(cmp(data[j], data[a]) < 0) {
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
if i > j {
|
if i > j {
|
||||||
@@ -151,10 +151,10 @@ func partitionLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool)
|
|||||||
j--
|
j--
|
||||||
|
|
||||||
for {
|
for {
|
||||||
for i <= j && less(data[i], data[a]) {
|
for i <= j && (cmp(data[i], data[a]) < 0) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
for i <= j && !less(data[j], data[a]) {
|
for i <= j && !(cmp(data[j], data[a]) < 0) {
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
if i > j {
|
if i > j {
|
||||||
@@ -168,17 +168,17 @@ func partitionLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool)
|
|||||||
return j, false
|
return j, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// partitionEqualLessFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot].
|
// partitionEqualCmpFunc partitions data[a:b] into elements equal to data[pivot] followed by elements greater than data[pivot].
|
||||||
// It assumed that data[a:b] does not contain elements smaller than the data[pivot].
|
// It assumed that data[a:b] does not contain elements smaller than the data[pivot].
|
||||||
func partitionEqualLessFunc[E any](data []E, a, b, pivot int, less func(a, b E) bool) (newpivot int) {
|
func partitionEqualCmpFunc[E any](data []E, a, b, pivot int, cmp func(a, b E) int) (newpivot int) {
|
||||||
data[a], data[pivot] = data[pivot], data[a]
|
data[a], data[pivot] = data[pivot], data[a]
|
||||||
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
||||||
|
|
||||||
for {
|
for {
|
||||||
for i <= j && !less(data[a], data[i]) {
|
for i <= j && !(cmp(data[a], data[i]) < 0) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
for i <= j && less(data[a], data[j]) {
|
for i <= j && (cmp(data[a], data[j]) < 0) {
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
if i > j {
|
if i > j {
|
||||||
@@ -191,15 +191,15 @@ func partitionEqualLessFunc[E any](data []E, a, b, pivot int, less func(a, b E)
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
// partialInsertionSortLessFunc partially sorts a slice, returns true if the slice is sorted at the end.
|
// partialInsertionSortCmpFunc partially sorts a slice, returns true if the slice is sorted at the end.
|
||||||
func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) bool {
|
func partialInsertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) bool {
|
||||||
const (
|
const (
|
||||||
maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted
|
maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted
|
||||||
shortestShifting = 50 // don't shift any elements on short arrays
|
shortestShifting = 50 // don't shift any elements on short arrays
|
||||||
)
|
)
|
||||||
i := a + 1
|
i := a + 1
|
||||||
for j := 0; j < maxSteps; j++ {
|
for j := 0; j < maxSteps; j++ {
|
||||||
for i < b && !less(data[i], data[i-1]) {
|
for i < b && !(cmp(data[i], data[i-1]) < 0) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) b
|
|||||||
// Shift the smaller one to the left.
|
// Shift the smaller one to the left.
|
||||||
if i-a >= 2 {
|
if i-a >= 2 {
|
||||||
for j := i - 1; j >= 1; j-- {
|
for j := i - 1; j >= 1; j-- {
|
||||||
if !less(data[j], data[j-1]) {
|
if !(cmp(data[j], data[j-1]) < 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
data[j], data[j-1] = data[j-1], data[j]
|
||||||
@@ -225,7 +225,7 @@ func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) b
|
|||||||
// Shift the greater one to the right.
|
// Shift the greater one to the right.
|
||||||
if b-i >= 2 {
|
if b-i >= 2 {
|
||||||
for j := i + 1; j < b; j++ {
|
for j := i + 1; j < b; j++ {
|
||||||
if !less(data[j], data[j-1]) {
|
if !(cmp(data[j], data[j-1]) < 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
data[j], data[j-1] = data[j-1], data[j]
|
||||||
@@ -235,9 +235,9 @@ func partialInsertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// breakPatternsLessFunc scatters some elements around in an attempt to break some patterns
|
// breakPatternsCmpFunc scatters some elements around in an attempt to break some patterns
|
||||||
// that might cause imbalanced partitions in quicksort.
|
// that might cause imbalanced partitions in quicksort.
|
||||||
func breakPatternsLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
func breakPatternsCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
||||||
length := b - a
|
length := b - a
|
||||||
if length >= 8 {
|
if length >= 8 {
|
||||||
random := xorshift(length)
|
random := xorshift(length)
|
||||||
@@ -253,12 +253,12 @@ func breakPatternsLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// choosePivotLessFunc chooses a pivot in data[a:b].
|
// choosePivotCmpFunc chooses a pivot in data[a:b].
|
||||||
//
|
//
|
||||||
// [0,8): chooses a static pivot.
|
// [0,8): chooses a static pivot.
|
||||||
// [8,shortestNinther): uses the simple median-of-three method.
|
// [8,shortestNinther): uses the simple median-of-three method.
|
||||||
// [shortestNinther,∞): uses the Tukey ninther method.
|
// [shortestNinther,∞): uses the Tukey ninther method.
|
||||||
func choosePivotLessFunc[E any](data []E, a, b int, less func(a, b E) bool) (pivot int, hint sortedHint) {
|
func choosePivotCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) (pivot int, hint sortedHint) {
|
||||||
const (
|
const (
|
||||||
shortestNinther = 50
|
shortestNinther = 50
|
||||||
maxSwaps = 4 * 3
|
maxSwaps = 4 * 3
|
||||||
@@ -276,12 +276,12 @@ func choosePivotLessFunc[E any](data []E, a, b int, less func(a, b E) bool) (piv
|
|||||||
if l >= 8 {
|
if l >= 8 {
|
||||||
if l >= shortestNinther {
|
if l >= shortestNinther {
|
||||||
// Tukey ninther method, the idea came from Rust's implementation.
|
// Tukey ninther method, the idea came from Rust's implementation.
|
||||||
i = medianAdjacentLessFunc(data, i, &swaps, less)
|
i = medianAdjacentCmpFunc(data, i, &swaps, cmp)
|
||||||
j = medianAdjacentLessFunc(data, j, &swaps, less)
|
j = medianAdjacentCmpFunc(data, j, &swaps, cmp)
|
||||||
k = medianAdjacentLessFunc(data, k, &swaps, less)
|
k = medianAdjacentCmpFunc(data, k, &swaps, cmp)
|
||||||
}
|
}
|
||||||
// Find the median among i, j, k and stores it into j.
|
// Find the median among i, j, k and stores it into j.
|
||||||
j = medianLessFunc(data, i, j, k, &swaps, less)
|
j = medianCmpFunc(data, i, j, k, &swaps, cmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch swaps {
|
switch swaps {
|
||||||
@@ -294,29 +294,29 @@ func choosePivotLessFunc[E any](data []E, a, b int, less func(a, b E) bool) (piv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// order2LessFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
|
// order2CmpFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
|
||||||
func order2LessFunc[E any](data []E, a, b int, swaps *int, less func(a, b E) bool) (int, int) {
|
func order2CmpFunc[E any](data []E, a, b int, swaps *int, cmp func(a, b E) int) (int, int) {
|
||||||
if less(data[b], data[a]) {
|
if cmp(data[b], data[a]) < 0 {
|
||||||
*swaps++
|
*swaps++
|
||||||
return b, a
|
return b, a
|
||||||
}
|
}
|
||||||
return a, b
|
return a, b
|
||||||
}
|
}
|
||||||
|
|
||||||
// medianLessFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c.
|
// medianCmpFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c.
|
||||||
func medianLessFunc[E any](data []E, a, b, c int, swaps *int, less func(a, b E) bool) int {
|
func medianCmpFunc[E any](data []E, a, b, c int, swaps *int, cmp func(a, b E) int) int {
|
||||||
a, b = order2LessFunc(data, a, b, swaps, less)
|
a, b = order2CmpFunc(data, a, b, swaps, cmp)
|
||||||
b, c = order2LessFunc(data, b, c, swaps, less)
|
b, c = order2CmpFunc(data, b, c, swaps, cmp)
|
||||||
a, b = order2LessFunc(data, a, b, swaps, less)
|
a, b = order2CmpFunc(data, a, b, swaps, cmp)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// medianAdjacentLessFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a.
|
// medianAdjacentCmpFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a.
|
||||||
func medianAdjacentLessFunc[E any](data []E, a int, swaps *int, less func(a, b E) bool) int {
|
func medianAdjacentCmpFunc[E any](data []E, a int, swaps *int, cmp func(a, b E) int) int {
|
||||||
return medianLessFunc(data, a-1, a, a+1, swaps, less)
|
return medianCmpFunc(data, a-1, a, a+1, swaps, cmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reverseRangeLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
func reverseRangeCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
||||||
i := a
|
i := a
|
||||||
j := b - 1
|
j := b - 1
|
||||||
for i < j {
|
for i < j {
|
||||||
@@ -326,37 +326,37 @@ func reverseRangeLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func swapRangeLessFunc[E any](data []E, a, b, n int, less func(a, b E) bool) {
|
func swapRangeCmpFunc[E any](data []E, a, b, n int, cmp func(a, b E) int) {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
data[a+i], data[b+i] = data[b+i], data[a+i]
|
data[a+i], data[b+i] = data[b+i], data[a+i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stableLessFunc[E any](data []E, n int, less func(a, b E) bool) {
|
func stableCmpFunc[E any](data []E, n int, cmp func(a, b E) int) {
|
||||||
blockSize := 20 // must be > 0
|
blockSize := 20 // must be > 0
|
||||||
a, b := 0, blockSize
|
a, b := 0, blockSize
|
||||||
for b <= n {
|
for b <= n {
|
||||||
insertionSortLessFunc(data, a, b, less)
|
insertionSortCmpFunc(data, a, b, cmp)
|
||||||
a = b
|
a = b
|
||||||
b += blockSize
|
b += blockSize
|
||||||
}
|
}
|
||||||
insertionSortLessFunc(data, a, n, less)
|
insertionSortCmpFunc(data, a, n, cmp)
|
||||||
|
|
||||||
for blockSize < n {
|
for blockSize < n {
|
||||||
a, b = 0, 2*blockSize
|
a, b = 0, 2*blockSize
|
||||||
for b <= n {
|
for b <= n {
|
||||||
symMergeLessFunc(data, a, a+blockSize, b, less)
|
symMergeCmpFunc(data, a, a+blockSize, b, cmp)
|
||||||
a = b
|
a = b
|
||||||
b += 2 * blockSize
|
b += 2 * blockSize
|
||||||
}
|
}
|
||||||
if m := a + blockSize; m < n {
|
if m := a + blockSize; m < n {
|
||||||
symMergeLessFunc(data, a, m, n, less)
|
symMergeCmpFunc(data, a, m, n, cmp)
|
||||||
}
|
}
|
||||||
blockSize *= 2
|
blockSize *= 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// symMergeLessFunc merges the two sorted subsequences data[a:m] and data[m:b] using
|
// symMergeCmpFunc merges the two sorted subsequences data[a:m] and data[m:b] using
|
||||||
// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum
|
// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum
|
||||||
// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz
|
// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz
|
||||||
// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in
|
// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in
|
||||||
@@ -375,7 +375,7 @@ func stableLessFunc[E any](data []E, n int, less func(a, b E) bool) {
|
|||||||
// symMerge assumes non-degenerate arguments: a < m && m < b.
|
// symMerge assumes non-degenerate arguments: a < m && m < b.
|
||||||
// Having the caller check this condition eliminates many leaf recursion calls,
|
// Having the caller check this condition eliminates many leaf recursion calls,
|
||||||
// which improves performance.
|
// which improves performance.
|
||||||
func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
func symMergeCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) {
|
||||||
// Avoid unnecessary recursions of symMerge
|
// Avoid unnecessary recursions of symMerge
|
||||||
// by direct insertion of data[a] into data[m:b]
|
// by direct insertion of data[a] into data[m:b]
|
||||||
// if data[a:m] only contains one element.
|
// if data[a:m] only contains one element.
|
||||||
@@ -387,7 +387,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
|||||||
j := b
|
j := b
|
||||||
for i < j {
|
for i < j {
|
||||||
h := int(uint(i+j) >> 1)
|
h := int(uint(i+j) >> 1)
|
||||||
if less(data[h], data[a]) {
|
if cmp(data[h], data[a]) < 0 {
|
||||||
i = h + 1
|
i = h + 1
|
||||||
} else {
|
} else {
|
||||||
j = h
|
j = h
|
||||||
@@ -411,7 +411,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
|||||||
j := m
|
j := m
|
||||||
for i < j {
|
for i < j {
|
||||||
h := int(uint(i+j) >> 1)
|
h := int(uint(i+j) >> 1)
|
||||||
if !less(data[m], data[h]) {
|
if !(cmp(data[m], data[h]) < 0) {
|
||||||
i = h + 1
|
i = h + 1
|
||||||
} else {
|
} else {
|
||||||
j = h
|
j = h
|
||||||
@@ -438,7 +438,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
|||||||
|
|
||||||
for start < r {
|
for start < r {
|
||||||
c := int(uint(start+r) >> 1)
|
c := int(uint(start+r) >> 1)
|
||||||
if !less(data[p-c], data[c]) {
|
if !(cmp(data[p-c], data[c]) < 0) {
|
||||||
start = c + 1
|
start = c + 1
|
||||||
} else {
|
} else {
|
||||||
r = c
|
r = c
|
||||||
@@ -447,33 +447,33 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
|||||||
|
|
||||||
end := n - start
|
end := n - start
|
||||||
if start < m && m < end {
|
if start < m && m < end {
|
||||||
rotateLessFunc(data, start, m, end, less)
|
rotateCmpFunc(data, start, m, end, cmp)
|
||||||
}
|
}
|
||||||
if a < start && start < mid {
|
if a < start && start < mid {
|
||||||
symMergeLessFunc(data, a, start, mid, less)
|
symMergeCmpFunc(data, a, start, mid, cmp)
|
||||||
}
|
}
|
||||||
if mid < end && end < b {
|
if mid < end && end < b {
|
||||||
symMergeLessFunc(data, mid, end, b, less)
|
symMergeCmpFunc(data, mid, end, b, cmp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rotateLessFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data:
|
// rotateCmpFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data:
|
||||||
// Data of the form 'x u v y' is changed to 'x v u y'.
|
// Data of the form 'x u v y' is changed to 'x v u y'.
|
||||||
// rotate performs at most b-a many calls to data.Swap,
|
// rotate performs at most b-a many calls to data.Swap,
|
||||||
// and it assumes non-degenerate arguments: a < m && m < b.
|
// and it assumes non-degenerate arguments: a < m && m < b.
|
||||||
func rotateLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
func rotateCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) {
|
||||||
i := m - a
|
i := m - a
|
||||||
j := b - m
|
j := b - m
|
||||||
|
|
||||||
for i != j {
|
for i != j {
|
||||||
if i > j {
|
if i > j {
|
||||||
swapRangeLessFunc(data, m-i, m, j, less)
|
swapRangeCmpFunc(data, m-i, m, j, cmp)
|
||||||
i -= j
|
i -= j
|
||||||
} else {
|
} else {
|
||||||
swapRangeLessFunc(data, m-i, m+j-i, i, less)
|
swapRangeCmpFunc(data, m-i, m+j-i, i, cmp)
|
||||||
j -= i
|
j -= i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// i == j
|
// i == j
|
||||||
swapRangeLessFunc(data, m-i, m, i, less)
|
swapRangeCmpFunc(data, m-i, m, i, cmp)
|
||||||
}
|
}
|
||||||
34
vendor/golang.org/x/exp/slices/zsortordered.go
generated
vendored
34
vendor/golang.org/x/exp/slices/zsortordered.go
generated
vendored
@@ -11,7 +11,7 @@ import "golang.org/x/exp/constraints"
|
|||||||
// insertionSortOrdered sorts data[a:b] using insertion sort.
|
// insertionSortOrdered sorts data[a:b] using insertion sort.
|
||||||
func insertionSortOrdered[E constraints.Ordered](data []E, a, b int) {
|
func insertionSortOrdered[E constraints.Ordered](data []E, a, b int) {
|
||||||
for i := a + 1; i < b; i++ {
|
for i := a + 1; i < b; i++ {
|
||||||
for j := i; j > a && (data[j] < data[j-1]); j-- {
|
for j := i; j > a && cmpLess(data[j], data[j-1]); j-- {
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
data[j], data[j-1] = data[j-1], data[j]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,10 @@ func siftDownOrdered[E constraints.Ordered](data []E, lo, hi, first int) {
|
|||||||
if child >= hi {
|
if child >= hi {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if child+1 < hi && (data[first+child] < data[first+child+1]) {
|
if child+1 < hi && cmpLess(data[first+child], data[first+child+1]) {
|
||||||
child++
|
child++
|
||||||
}
|
}
|
||||||
if !(data[first+root] < data[first+child]) {
|
if !cmpLess(data[first+root], data[first+child]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data[first+root], data[first+child] = data[first+child], data[first+root]
|
data[first+root], data[first+child] = data[first+child], data[first+root]
|
||||||
@@ -107,7 +107,7 @@ func pdqsortOrdered[E constraints.Ordered](data []E, a, b, limit int) {
|
|||||||
|
|
||||||
// Probably the slice contains many duplicate elements, partition the slice into
|
// Probably the slice contains many duplicate elements, partition the slice into
|
||||||
// elements equal to and elements greater than the pivot.
|
// elements equal to and elements greater than the pivot.
|
||||||
if a > 0 && !(data[a-1] < data[pivot]) {
|
if a > 0 && !cmpLess(data[a-1], data[pivot]) {
|
||||||
mid := partitionEqualOrdered(data, a, b, pivot)
|
mid := partitionEqualOrdered(data, a, b, pivot)
|
||||||
a = mid
|
a = mid
|
||||||
continue
|
continue
|
||||||
@@ -138,10 +138,10 @@ func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivo
|
|||||||
data[a], data[pivot] = data[pivot], data[a]
|
data[a], data[pivot] = data[pivot], data[a]
|
||||||
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
||||||
|
|
||||||
for i <= j && (data[i] < data[a]) {
|
for i <= j && cmpLess(data[i], data[a]) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
for i <= j && !(data[j] < data[a]) {
|
for i <= j && !cmpLess(data[j], data[a]) {
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
if i > j {
|
if i > j {
|
||||||
@@ -153,10 +153,10 @@ func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivo
|
|||||||
j--
|
j--
|
||||||
|
|
||||||
for {
|
for {
|
||||||
for i <= j && (data[i] < data[a]) {
|
for i <= j && cmpLess(data[i], data[a]) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
for i <= j && !(data[j] < data[a]) {
|
for i <= j && !cmpLess(data[j], data[a]) {
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
if i > j {
|
if i > j {
|
||||||
@@ -177,10 +177,10 @@ func partitionEqualOrdered[E constraints.Ordered](data []E, a, b, pivot int) (ne
|
|||||||
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
||||||
|
|
||||||
for {
|
for {
|
||||||
for i <= j && !(data[a] < data[i]) {
|
for i <= j && !cmpLess(data[a], data[i]) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
for i <= j && (data[a] < data[j]) {
|
for i <= j && cmpLess(data[a], data[j]) {
|
||||||
j--
|
j--
|
||||||
}
|
}
|
||||||
if i > j {
|
if i > j {
|
||||||
@@ -201,7 +201,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
|
|||||||
)
|
)
|
||||||
i := a + 1
|
i := a + 1
|
||||||
for j := 0; j < maxSteps; j++ {
|
for j := 0; j < maxSteps; j++ {
|
||||||
for i < b && !(data[i] < data[i-1]) {
|
for i < b && !cmpLess(data[i], data[i-1]) {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
|
|||||||
// Shift the smaller one to the left.
|
// Shift the smaller one to the left.
|
||||||
if i-a >= 2 {
|
if i-a >= 2 {
|
||||||
for j := i - 1; j >= 1; j-- {
|
for j := i - 1; j >= 1; j-- {
|
||||||
if !(data[j] < data[j-1]) {
|
if !cmpLess(data[j], data[j-1]) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
data[j], data[j-1] = data[j-1], data[j]
|
||||||
@@ -227,7 +227,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
|
|||||||
// Shift the greater one to the right.
|
// Shift the greater one to the right.
|
||||||
if b-i >= 2 {
|
if b-i >= 2 {
|
||||||
for j := i + 1; j < b; j++ {
|
for j := i + 1; j < b; j++ {
|
||||||
if !(data[j] < data[j-1]) {
|
if !cmpLess(data[j], data[j-1]) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
data[j], data[j-1] = data[j-1], data[j]
|
||||||
@@ -298,7 +298,7 @@ func choosePivotOrdered[E constraints.Ordered](data []E, a, b int) (pivot int, h
|
|||||||
|
|
||||||
// order2Ordered returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
|
// order2Ordered returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
|
||||||
func order2Ordered[E constraints.Ordered](data []E, a, b int, swaps *int) (int, int) {
|
func order2Ordered[E constraints.Ordered](data []E, a, b int, swaps *int) (int, int) {
|
||||||
if data[b] < data[a] {
|
if cmpLess(data[b], data[a]) {
|
||||||
*swaps++
|
*swaps++
|
||||||
return b, a
|
return b, a
|
||||||
}
|
}
|
||||||
@@ -389,7 +389,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
|||||||
j := b
|
j := b
|
||||||
for i < j {
|
for i < j {
|
||||||
h := int(uint(i+j) >> 1)
|
h := int(uint(i+j) >> 1)
|
||||||
if data[h] < data[a] {
|
if cmpLess(data[h], data[a]) {
|
||||||
i = h + 1
|
i = h + 1
|
||||||
} else {
|
} else {
|
||||||
j = h
|
j = h
|
||||||
@@ -413,7 +413,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
|||||||
j := m
|
j := m
|
||||||
for i < j {
|
for i < j {
|
||||||
h := int(uint(i+j) >> 1)
|
h := int(uint(i+j) >> 1)
|
||||||
if !(data[m] < data[h]) {
|
if !cmpLess(data[m], data[h]) {
|
||||||
i = h + 1
|
i = h + 1
|
||||||
} else {
|
} else {
|
||||||
j = h
|
j = h
|
||||||
@@ -440,7 +440,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
|||||||
|
|
||||||
for start < r {
|
for start < r {
|
||||||
c := int(uint(start+r) >> 1)
|
c := int(uint(start+r) >> 1)
|
||||||
if !(data[p-c] < data[c]) {
|
if !cmpLess(data[p-c], data[c]) {
|
||||||
start = c + 1
|
start = c + 1
|
||||||
} else {
|
} else {
|
||||||
r = c
|
r = c
|
||||||
|
|||||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
@@ -1,7 +1,7 @@
|
|||||||
# github.com/davecgh/go-spew v1.1.1
|
# github.com/davecgh/go-spew v1.1.1
|
||||||
## explicit
|
## explicit
|
||||||
github.com/davecgh/go-spew/spew
|
github.com/davecgh/go-spew/spew
|
||||||
# github.com/eleksir/go-xmpp v1.3.0
|
# github.com/eleksir/go-xmpp v1.4.0
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/eleksir/go-xmpp
|
github.com/eleksir/go-xmpp
|
||||||
# github.com/hjson/hjson-go v3.3.0+incompatible
|
# github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
@@ -13,7 +13,7 @@ github.com/sirupsen/logrus
|
|||||||
# golang.org/x/crypto v0.11.0
|
# golang.org/x/crypto v0.11.0
|
||||||
## explicit; go 1.17
|
## explicit; go 1.17
|
||||||
golang.org/x/crypto/pbkdf2
|
golang.org/x/crypto/pbkdf2
|
||||||
# golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
# golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
golang.org/x/exp/constraints
|
golang.org/x/exp/constraints
|
||||||
golang.org/x/exp/slices
|
golang.org/x/exp/slices
|
||||||
|
|||||||
Reference in New Issue
Block a user