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
|
||||
|
||||
GOOPTS=CGO_ENABLED=0
|
||||
CGO_ENABLED=0
|
||||
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
||||
|
||||
all: clean build
|
||||
|
||||
build:
|
||||
${GOOPTS} go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go \
|
||||
buny.go main.go
|
||||
go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
|
||||
|
||||
clean:
|
||||
go clean
|
||||
|
||||
41
README.md
41
README.md
@@ -2,13 +2,16 @@
|
||||
|
||||
Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата.
|
||||
|
||||
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, который следует себе задать перед его
|
||||
использованием - а не забаню ли я кого-то не того?
|
||||
|
||||
## Что он может?
|
||||
|
||||
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. Шифрование работает по
|
||||
классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде), без поддержки
|
||||
механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
|
||||
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. (Пока что) Шифрование
|
||||
работает по классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде),
|
||||
без поддержки механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
|
||||
|
||||
Имеет возможность заносить пользователей в бан-лист согласно заданным в конфиге правилам.
|
||||
Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам.
|
||||
|
||||
## Что он не может?
|
||||
|
||||
@@ -16,13 +19,9 @@
|
||||
* Не умеет разгадывать капчу.
|
||||
|
||||
## Как заставить его работать?
|
||||
Технически, бот собирается гошкой 1.20 под платформу linux. Работоспособность на других платформах не тестировалась.
|
||||
|
||||
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, кторый следует себе задать еред его
|
||||
использоованием - а не забаню ли я кого-то не того?
|
||||
|
||||
Технически, бот собирается гошкой 1.20 поод платформу linux. Работоспособность на других платформах не тестировалась.
|
||||
|
||||
Чтобы получить готовые бинарники достаточно выполнить команду:
|
||||
Чтобы получить готовые бинарники, достаточно выполнить команду:
|
||||
|
||||
```bash
|
||||
make
|
||||
@@ -30,8 +29,8 @@ make
|
||||
|
||||
в итоге получится бинарник **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?
|
||||
|
||||
Да, бывают ситуации, когда по ошибке бот может забанить кого-то не того или, если не настроен белый список вообще ни в
|
||||
каком виде, то будет банить всех входящих. И если вдруг произошло так, что в бан попали все админы комнаты, то разбанить
|
||||
их может либо овнер (никто же не даст овнера боту), либо тот, кто зайдёт в комнату из-под аккаунта бота и разбанить всех
|
||||
от его имени.
|
||||
каком виде, то будет банить всех входящих.
|
||||
|
||||
В далёких планах есть идея релизовать очистку бан-листа при запуске бота с настройкой `"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"
|
||||
)
|
||||
|
||||
func buny(v xmpp.Presence) error {
|
||||
func buny(v xmpp.Presence) error { //nolint:gocognit
|
||||
var err error
|
||||
|
||||
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
|
||||
@@ -24,7 +24,7 @@ func buny(v xmpp.Presence) error {
|
||||
room := strings.SplitN(v.From, "/", 2)[0]
|
||||
|
||||
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
|
||||
}
|
||||
@@ -58,14 +58,13 @@ func buny(v xmpp.Presence) error {
|
||||
for _, cRoom := range roomsConnected {
|
||||
if cRoom == room {
|
||||
for _, bEntry := range blackList.Blacklist {
|
||||
|
||||
// Обработаем правила гобального чёрного списка
|
||||
// Обработаем правила глобального чёрного списка
|
||||
if bEntry.RoomName == "" {
|
||||
for _, jid_regexp := range bEntry.JidRe {
|
||||
re, err := regexp.Compile(jid_regexp)
|
||||
for _, jidRegexp := range bEntry.JidRe {
|
||||
re, err := regexp.Compile(jidRegexp)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp)
|
||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -91,7 +90,6 @@ func buny(v xmpp.Presence) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -99,11 +97,11 @@ func buny(v xmpp.Presence) error {
|
||||
|
||||
// Обработаем правила конкретного канала, комнаты конференции
|
||||
if bEntry.RoomName == room {
|
||||
for _, jid_regexp := range bEntry.JidRe {
|
||||
re, err := regexp.Compile(jid_regexp)
|
||||
for _, jidRegexp := range bEntry.JidRe {
|
||||
re, err := regexp.Compile(jidRegexp)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jid_regexp)
|
||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -129,7 +127,6 @@ func buny(v xmpp.Presence) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -141,3 +138,5 @@ func buny(v xmpp.Presence) error {
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
|
||||
@@ -4,27 +4,27 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Collection это sync.Map-ка, хранящая в себе произвольные итемы
|
||||
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
|
||||
type Collection struct {
|
||||
items sync.Map
|
||||
close chan struct{}
|
||||
}
|
||||
|
||||
// Item представляет собой интерфейс для хранения произвольных данных
|
||||
// Item представляет собой интерфейс для хранения произвольных данных.
|
||||
type item struct {
|
||||
data interface{}
|
||||
}
|
||||
|
||||
// NewCollection создаёт инстанс коллекции
|
||||
// NewCollection создаёт инстанс коллекции.
|
||||
func NewCollection() *Collection {
|
||||
c := &Collection{
|
||||
c := &Collection{ //nolint:exhaustruct
|
||||
close: make(chan struct{}),
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Get достаёт данные по заданному ключу из коллекции
|
||||
// (collection *Collection) Get(key interface{}) (interface{}, bool) достаёт данные по заданному ключу из коллекции.
|
||||
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
||||
obj, exists := collection.items.Load(key)
|
||||
|
||||
@@ -37,14 +37,14 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
||||
return item.data, true
|
||||
}
|
||||
|
||||
// Set сохраняет данные с заданным ключом в коллекцию
|
||||
// (collection *Collection) Set(key interface{}, value interface{}) сохраняет данные с заданным ключом в коллекцию.
|
||||
func (collection *Collection) Set(key interface{}, value interface{}) {
|
||||
collection.items.Store(key, item{
|
||||
data: value,
|
||||
})
|
||||
}
|
||||
|
||||
// Range применяет функцию f ко всем ключам в коллекции
|
||||
// (collection *Collection) Range(f func(key, value interface{}) bool) применяет функцию f ко всем ключам в коллекции.
|
||||
func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
||||
fn := func(key, value interface{}) bool {
|
||||
item := value.(item)
|
||||
@@ -55,12 +55,12 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
||||
collection.items.Range(fn)
|
||||
}
|
||||
|
||||
// Delete удаляет ключ и значение из коллекции данных
|
||||
// (collection *Collection) Delete(key interface{}) удаляет ключ и значение из коллекции данных.
|
||||
func (collection *Collection) Delete(key interface{}) {
|
||||
collection.items.Delete(key)
|
||||
}
|
||||
|
||||
// Close очищает и высвобождает ресурсы, занятые коллекцией
|
||||
// (collection *Collection) Close() очищает и высвобождает ресурсы, занятые коллекцией.
|
||||
func (collection *Collection) Close() {
|
||||
collection.close <- struct{}{}
|
||||
collection.items = sync.Map{}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func parseEvent(e interface{}) {
|
||||
func parseEvent(e interface{}) { //nolint:gocognit,gocyclo
|
||||
lastServerActivity = time.Now().Unix()
|
||||
|
||||
switch v := e.(type) {
|
||||
@@ -30,7 +30,7 @@ func parseEvent(e interface{}) {
|
||||
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text
|
||||
if v.Text != "" {
|
||||
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
|
||||
switch v.Type { //nolint:whitespace
|
||||
switch v.Type { //nolint:wsl,whitespace
|
||||
|
||||
// Групповой чятик
|
||||
case "groupchat":
|
||||
@@ -87,16 +87,18 @@ func parseEvent(e interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
var iqStruct jabberSimpleIqGetQuery
|
||||
var parseError bool
|
||||
var (
|
||||
iqStruct jabberSimpleIqGetQuery
|
||||
parseError bool
|
||||
)
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||
switch iqStruct.Xmlns { //nolint:whitespace
|
||||
switch iqStruct.Xmlns { //nolint:wsl,whitespace
|
||||
|
||||
// Запрос номера версии приложения
|
||||
case "jabber:iq:version":
|
||||
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 += "<version>1.с_чем-то</version>"
|
||||
answer += "<os>линупс</os>"
|
||||
@@ -109,7 +111,7 @@ func parseEvent(e interface{}) {
|
||||
xmpp.IQTypeResult,
|
||||
answer,
|
||||
); 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)
|
||||
}
|
||||
|
||||
@@ -186,9 +188,9 @@ func parseEvent(e interface{}) {
|
||||
|
||||
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\" "
|
||||
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 += "</error>"
|
||||
answer += "</error>" //nolint:goconst
|
||||
|
||||
if id, err := talk.RawInformation(
|
||||
v.To,
|
||||
@@ -287,7 +289,7 @@ func parseEvent(e interface{}) {
|
||||
default:
|
||||
log.Info("Got unknown IQ get pubsub something request, discarding")
|
||||
log.Info(spew.Sdump(e))
|
||||
parseError = true
|
||||
parseError = true //nolint:wsl
|
||||
}
|
||||
} else {
|
||||
parseError = true
|
||||
@@ -311,6 +313,7 @@ func parseEvent(e interface{}) {
|
||||
"",
|
||||
); err != nil {
|
||||
log.Errorf("Unable to send pong to jabber server: id=%s, err=%s", id, err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -335,7 +338,7 @@ func parseEvent(e interface{}) {
|
||||
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
||||
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)
|
||||
serverPingTimestampRx = time.Now().Unix()
|
||||
serverPingTimestampRx = time.Now().Unix() //nolint:wsl
|
||||
|
||||
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
|
||||
case v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||
@@ -405,8 +408,10 @@ func parseEvent(e interface{}) {
|
||||
case xmpp.IQTypeError:
|
||||
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
||||
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
||||
var iqPingStruct jabberIqPing
|
||||
var parseError = true
|
||||
var (
|
||||
iqPingStruct jabberIqPing
|
||||
parseError = true
|
||||
)
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
||||
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
||||
@@ -429,7 +434,7 @@ func parseEvent(e interface{}) {
|
||||
|
||||
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
|
||||
if parseError {
|
||||
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAccepatble
|
||||
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable
|
||||
|
||||
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
||||
if v.To == talk.JID() {
|
||||
@@ -503,8 +508,8 @@ func parseEvent(e interface{}) {
|
||||
log.Errorf(spew.Sdump(v))
|
||||
}
|
||||
} else {
|
||||
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affilation: %s, Role: %s, JID: %s",
|
||||
v.Type, v.From, v.To, v.Show, v.Status, v.Affilation, v.Role, v.JID)
|
||||
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.Affiliation, v.Role, v.JID)
|
||||
|
||||
// Это наш собственный Presence
|
||||
if v.Show == "" && v.Status == "" {
|
||||
@@ -544,7 +549,7 @@ func parseEvent(e interface{}) {
|
||||
|
||||
for _, feature := range v.Features {
|
||||
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)
|
||||
|
||||
36
globals.go
36
globals.go
@@ -6,60 +6,60 @@ import (
|
||||
"github.com/eleksir/go-xmpp"
|
||||
)
|
||||
|
||||
// Config - это у нас глобальная штука :)
|
||||
// Config - это у нас глобальная штука :) .
|
||||
var config myConfig
|
||||
|
||||
// Списки тех, кого мы не баним никогда
|
||||
// Списки тех, кого мы не баним никогда.
|
||||
var whiteList myWhiteList
|
||||
|
||||
// Списки злодеев, которых мы банм
|
||||
// Списки злодеев, которых мы баним.
|
||||
var blackList myBlackList
|
||||
|
||||
// Ставится в true, если мы получили сигнал на выключение
|
||||
// Ставится в true, если мы получили сигнал на выключение.
|
||||
var shutdown = false
|
||||
|
||||
// Чтобы не организовывать драку за установку коннекта
|
||||
// Чтобы не организовывать драку за установку коннекта.
|
||||
var connecting = false
|
||||
|
||||
// Глобальное состояние соединения
|
||||
// Глобальное состояние соединения.
|
||||
var isConnected = false
|
||||
|
||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов
|
||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||
var sigChan = make(chan os.Signal, 1)
|
||||
|
||||
// Основной инстанс xmpp-клиента
|
||||
// Основной инстанс xmpp-клиента.
|
||||
var talk *xmpp.Client
|
||||
|
||||
// Опции подключения к xmpp-серверу
|
||||
// Опции подключения к xmpp-серверу.
|
||||
var options *xmpp.Options
|
||||
|
||||
// Список комнат, в которых находится бот
|
||||
// Список комнат, в которых находится бот.
|
||||
var roomsConnected []string
|
||||
|
||||
// Время последней активности, нужно для jabber:iq:last
|
||||
// Время последней активности, нужно для jabber:iq:last.
|
||||
var lastActivity int64
|
||||
|
||||
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
|
||||
// сервера
|
||||
// сервера.
|
||||
var lastServerActivity int64
|
||||
|
||||
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
||||
// muc-ов
|
||||
// muc-ов.
|
||||
var lastMucActivity = NewCollection()
|
||||
|
||||
// Получен ли ответ на запрос disco#info к серверу
|
||||
// Получен ли ответ на запрос disco#info к серверу.
|
||||
var serverCapsQueried bool
|
||||
|
||||
// sync.Map-ка с капабилити сервера
|
||||
// sync.Map-ка с капабилити сервера.
|
||||
var serverCapsList = NewCollection()
|
||||
|
||||
// sync.Map-ка с комнатами и их capability
|
||||
// sync.Map-ка с комнатами и их capability.
|
||||
var mucCapsList = NewCollection()
|
||||
|
||||
// Время, когда был отправлен c2s ping
|
||||
// Время, когда был отправлен c2s ping.
|
||||
var serverPingTimestampTx int64
|
||||
|
||||
// Время, когда был принят s2c pong
|
||||
// Время, когда был принят s2c pong.
|
||||
var serverPingTimestampRx int64
|
||||
|
||||
/* 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 (
|
||||
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/sirupsen/logrus v1.9.3
|
||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1
|
||||
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691
|
||||
)
|
||||
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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.3.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8=
|
||||
github.com/eleksir/go-xmpp v1.4.0 h1:CJ91zeoLoV0FqhhcvZnkBjeuaXWbWgRTJ/HrzKugM4s=
|
||||
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/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
||||
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=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
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-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
|
||||
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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
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"
|
||||
)
|
||||
|
||||
// Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения
|
||||
// sigHandler() Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||
func sigHandler() {
|
||||
log.Debug("Installing signal handler")
|
||||
|
||||
@@ -28,19 +28,19 @@ func sigHandler() {
|
||||
case syscall.SIGQUIT:
|
||||
log.Infoln("Got SIGQUIT, quitting")
|
||||
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true
|
||||
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true.
|
||||
shutdown = true
|
||||
|
||||
if isConnected && !shutdown {
|
||||
log.Debug("Try to set our presence to Unavailable and status to Offline")
|
||||
|
||||
// Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в offline
|
||||
// Пока за неимением лучшего сообщим об этом самим себе
|
||||
// Пока за неимением лучшего сообщим об этом самим себе.
|
||||
for _, room := range roomsConnected {
|
||||
if _, err := talk.SendPresence(
|
||||
xmpp.Presence{ //nolint:exhaustruct
|
||||
@@ -53,7 +53,7 @@ func sigHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
// И закрываем соединение
|
||||
// И закрываем соединение.
|
||||
log.Debugf("Closing connection to jabber server")
|
||||
|
||||
if err := talk.Close(); err != nil {
|
||||
@@ -65,13 +65,16 @@ func sigHandler() {
|
||||
}
|
||||
}
|
||||
|
||||
// Читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге нет
|
||||
func readConfig() {
|
||||
// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
|
||||
// нет.
|
||||
func readConfig() { //nolint:gocognit
|
||||
configLoaded := false
|
||||
executablePath, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get current executable path: %s", err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
||||
@@ -86,12 +89,12 @@ func readConfig() {
|
||||
for _, location := range locations {
|
||||
fileInfo, err := os.Stat(location)
|
||||
|
||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||
if fileInfo.Size() > 65535 {
|
||||
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||
|
||||
@@ -100,7 +103,7 @@ func readConfig() {
|
||||
|
||||
buf, err := os.ReadFile(location)
|
||||
|
||||
// Не удалось прочитать, попробуем следующего кандидата
|
||||
// Не удалось прочитать, попробуем следующего кандидата.
|
||||
if err != nil {
|
||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||
|
||||
@@ -110,18 +113,21 @@ func readConfig() {
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||
var sampleConfig myConfig
|
||||
var tmp map[string]interface{}
|
||||
var (
|
||||
sampleConfig myConfig
|
||||
tmp map[string]interface{}
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить - попробуем следующего кандидата
|
||||
// Не удалось распарсить - попробуем следующего кандидата.
|
||||
if err != nil {
|
||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
tmpJSON, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json
|
||||
if err != nil {
|
||||
@@ -130,7 +136,7 @@ func readConfig() {
|
||||
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)
|
||||
|
||||
continue
|
||||
@@ -138,20 +144,22 @@ func readConfig() {
|
||||
|
||||
// Значения для Jabber-клиента
|
||||
if sampleConfig.Jabber.Server == "" {
|
||||
sampleConfig.Jabber.Server = "localhost"
|
||||
log.Errorf("Jabber server is not defined in config, using localhost")
|
||||
log.Error("Jabber server is not defined in config, using localhost")
|
||||
sampleConfig.Jabber.Server = "localhost" //nolint:wsl
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.Port == 0 {
|
||||
sampleConfig.Jabber.Port = 5552
|
||||
sampleConfig.Jabber.Port = 5222
|
||||
|
||||
if sampleConfig.Jabber.Ssl {
|
||||
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 {
|
||||
@@ -166,26 +174,38 @@ func readConfig() {
|
||||
|
||||
if sampleConfig.Jabber.ConnectionTimeout == 0 {
|
||||
sampleConfig.Jabber.ConnectionTimeout = 10
|
||||
|
||||
log.Info("Jabber server connection_timeout not defined in config, using 10 seconds")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.ReconnectDelay == 0 {
|
||||
sampleConfig.Jabber.ReconnectDelay = 3
|
||||
|
||||
log.Info("Jabber server reconnect_delay not defined in config, using 3 seconds")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.ServerPingDelay == 0 {
|
||||
sampleConfig.Jabber.ServerPingDelay = 60
|
||||
|
||||
log.Info("Jabber server_ping_delay not defined in config, using 60 seconds")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.MucPingDelay == 0 {
|
||||
sampleConfig.Jabber.MucPingDelay = 900
|
||||
|
||||
log.Info("Jabber muc_ping_delay not defined in config, using 900 seconds")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.MucRejoinDelay == 0 {
|
||||
sampleConfig.Jabber.MucRejoinDelay = 3
|
||||
|
||||
log.Info("Jabber muc_rejoin_delay not defined in config, using 3 seconds")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.PingSplayDelay == 0 {
|
||||
sampleConfig.Jabber.PingSplayDelay = 3
|
||||
|
||||
log.Info("Jabber ping_splay_delay not defined in config, using 3 seconds")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.Nick == "" {
|
||||
@@ -195,10 +215,14 @@ func readConfig() {
|
||||
|
||||
if sampleConfig.Jabber.Resource == "" {
|
||||
sampleConfig.Jabber.Resource = "buny bot"
|
||||
|
||||
log.Info("Jabber resource not defined in config, using buny bot")
|
||||
}
|
||||
|
||||
if sampleConfig.Jabber.User == "" {
|
||||
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 не задан, то авторизации не будет
|
||||
@@ -212,13 +236,15 @@ func readConfig() {
|
||||
|
||||
if sampleConfig.Loglevel == "" {
|
||||
sampleConfig.Loglevel = "info"
|
||||
|
||||
log.Info("loglevel not defined in config, using info")
|
||||
}
|
||||
|
||||
// sampleConfig.Log = "" if not set
|
||||
|
||||
config = sampleConfig
|
||||
configLoaded = true
|
||||
log.Infof("Using %s as config file", location)
|
||||
log.Infof("Using %s as config file", location) //nolint:wsl
|
||||
|
||||
break
|
||||
}
|
||||
@@ -229,13 +255,14 @@ func readConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Читает и валидирует белые списки пользователей
|
||||
// readWhitelist() читает и валидирует белые списки пользователей.
|
||||
func readWhitelist() {
|
||||
whitelistLoaded := false
|
||||
executablePath, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get current executable path: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
|
||||
@@ -274,8 +301,11 @@ func readWhitelist() {
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||
var sampleWhitelist myWhiteList
|
||||
var tmp map[string]interface{}
|
||||
var (
|
||||
sampleWhitelist myWhiteList
|
||||
tmp map[string]interface{}
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить - попробуем следующего кандидата
|
||||
@@ -285,7 +315,7 @@ func readWhitelist() {
|
||||
continue
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
tmpJSON, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json
|
||||
if err != nil {
|
||||
@@ -294,7 +324,7 @@ func readWhitelist() {
|
||||
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)
|
||||
|
||||
continue
|
||||
@@ -302,6 +332,7 @@ func readWhitelist() {
|
||||
|
||||
whiteList = sampleWhitelist
|
||||
whitelistLoaded = true
|
||||
|
||||
log.Infof("Using %s as whiteList file", location)
|
||||
|
||||
break
|
||||
@@ -309,17 +340,20 @@ func readWhitelist() {
|
||||
|
||||
if !whitelistLoaded {
|
||||
log.Error("Whitelist was not loaded! Refusing to start.")
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Читает и валидирует чёрные списки пользователей
|
||||
// readBlacklist() читает и валидирует чёрные списки пользователей.
|
||||
func readBlacklist() {
|
||||
blacklistLoaded := false
|
||||
executablePath, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get current executable path: %s", err)
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
|
||||
@@ -358,8 +392,11 @@ func readBlacklist() {
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||
var sampleBlacklist myBlackList
|
||||
var tmp map[string]interface{}
|
||||
var (
|
||||
sampleBlacklist myBlackList
|
||||
tmp map[string]interface{}
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить - попробуем следующего кандидата
|
||||
@@ -369,7 +406,7 @@ func readBlacklist() {
|
||||
continue
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
tmpJSON, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json
|
||||
if err != nil {
|
||||
@@ -378,7 +415,7 @@ func readBlacklist() {
|
||||
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)
|
||||
|
||||
continue
|
||||
@@ -386,6 +423,7 @@ func readBlacklist() {
|
||||
|
||||
blackList = sampleBlacklist
|
||||
blacklistLoaded = true
|
||||
|
||||
log.Infof("Using %s as blacklist file", location)
|
||||
|
||||
break
|
||||
@@ -393,10 +431,12 @@ func readBlacklist() {
|
||||
|
||||
if !blacklistLoaded {
|
||||
log.Error("Blacklist was not loaded! Refusing to start.")
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// establishConnection() устанавливает соединение с jabber-сервером.
|
||||
func establishConnection() {
|
||||
var err error
|
||||
|
||||
@@ -404,39 +444,32 @@ func establishConnection() {
|
||||
return
|
||||
}
|
||||
|
||||
// проставляем глобальные переменные
|
||||
// Проставляем глобальные переменные.
|
||||
connecting = true
|
||||
isConnected = false
|
||||
roomsConnected = make([]string, 0)
|
||||
|
||||
for {
|
||||
talk, err = options.NewClient()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
||||
os.Exit(1)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
||||
for {
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
|
||||
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
||||
|
||||
// Понятно, что мы уже не в процессе установления соединения, а произошла ошибка
|
||||
connecting = false
|
||||
os.Exit(1)
|
||||
} else {
|
||||
log.Info("Connected")
|
||||
|
||||
break
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
log.Info("Connected")
|
||||
|
||||
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
|
||||
for _, room := range config.Jabber.Channels {
|
||||
go joinMuc(room)
|
||||
@@ -467,22 +500,7 @@ func establishConnection() {
|
||||
}
|
||||
}
|
||||
|
||||
func reEstablishConnection() {
|
||||
// Серверные 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()
|
||||
}
|
||||
|
||||
// Джойнится к конференциям/каналам/комнатам в джаббере
|
||||
// joinMuc(room string) джойнится к конференциям/каналам/комнатам в джаббере.
|
||||
func joinMuc(room string) {
|
||||
log.Debugf("Sending disco#info from %s to %s", talk.JID(), room)
|
||||
|
||||
@@ -492,11 +510,13 @@ func joinMuc(room string) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Ждём, пока muc нам вернёт список фичей
|
||||
// Ждём, пока muc нам вернёт список фичей.
|
||||
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
|
||||
var myRoom interface{}
|
||||
var supported bool
|
||||
var exist bool
|
||||
var (
|
||||
myRoom interface{}
|
||||
supported bool
|
||||
exist bool
|
||||
)
|
||||
|
||||
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 {
|
||||
break
|
||||
} else {
|
||||
}
|
||||
|
||||
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := talk.JoinMUCNoHistory(room, config.Jabber.Nick); err != nil {
|
||||
log.Errorf("Unable to join to MUC: %s", room)
|
||||
|
||||
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++ {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if slices.Contains(roomsConnected, room) {
|
||||
entered = true
|
||||
|
||||
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)
|
||||
|
||||
if _, err := talk.SendPresence(
|
||||
@@ -547,8 +581,9 @@ func joinMuc(room string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяет живость соединения с сервером
|
||||
func probeServerLiveness() {
|
||||
// probeServerLiveness() проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
|
||||
// они выкидывают клиента через некоторое время неактивности.
|
||||
func probeServerLiveness() { //nolint:gocognit
|
||||
for {
|
||||
if shutdown {
|
||||
return
|
||||
@@ -569,8 +604,10 @@ func probeServerLiveness() {
|
||||
// }
|
||||
|
||||
if serverCapsQueried { // Сервер ответил на disco#info
|
||||
var value interface{}
|
||||
var exist bool
|
||||
var (
|
||||
value interface{}
|
||||
exist bool
|
||||
)
|
||||
|
||||
value, exist = serverCapsList.Get("urn:xmpp:ping")
|
||||
|
||||
@@ -602,18 +639,18 @@ func probeServerLiveness() {
|
||||
|
||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
||||
os.Exit(1)
|
||||
} else {
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
} else { // Первая пуля пока не вылетела, отправляем
|
||||
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 {
|
||||
os.Exit(1)
|
||||
} else {
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
|
||||
serverPingTimestampTx = time.Now().Unix()
|
||||
}
|
||||
|
||||
// Сервер не анонсировал, что умеет в c2s пинги
|
||||
@@ -622,8 +659,6 @@ func probeServerLiveness() {
|
||||
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
os.Exit(1)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else { // Сервер не ответил на disco#info
|
||||
@@ -631,28 +666,28 @@ func probeServerLiveness() {
|
||||
|
||||
if _, err := talk.SendKeepAlive(); err != nil {
|
||||
os.Exit(1)
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е
|
||||
// probeMUCLiveness() Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
||||
func probeMUCLiveness() {
|
||||
for {
|
||||
for _, room := range roomsConnected {
|
||||
var exist bool
|
||||
var lastActivityTs interface{}
|
||||
var (
|
||||
exist bool
|
||||
lastActivityTS interface{}
|
||||
)
|
||||
|
||||
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||
if lastActivityTs, exist = lastMucActivity.Get(room); !exist {
|
||||
if lastActivityTS, exist = lastMucActivity.Get(room); !exist {
|
||||
continue
|
||||
}
|
||||
|
||||
// Если время последней активности в чятике не превысило
|
||||
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем
|
||||
if (time.Now().Unix() - lastActivityTs.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
||||
// config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay, ничего не пингуем.
|
||||
if (time.Now().Unix() - lastActivityTS.(int64)) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -673,10 +708,10 @@ func probeMUCLiveness() {
|
||||
roomMap, exist = mucCapsList.Get(room)
|
||||
|
||||
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
|
||||
// Server Optimization
|
||||
// Server Optimization.
|
||||
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
|
||||
go func(room string) {
|
||||
// Небольшая рандомная задержка перед пингом комнаты
|
||||
// Небольшая рандомная задержка перед пингом комнаты.
|
||||
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||
time.Sleep(sleepTime)
|
||||
|
||||
|
||||
5
main.go
5
main.go
@@ -94,8 +94,8 @@ func main() {
|
||||
Debug: verboseClient,
|
||||
Session: false,
|
||||
Status: "xa",
|
||||
StatusMessage: "echo bot is starting up",
|
||||
DialTimeout: 10 * time.Second,
|
||||
StatusMessage: fmt.Sprintf("%s bot is starting up", config.Jabber.Nick),
|
||||
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
|
||||
}
|
||||
|
||||
go sigHandler()
|
||||
@@ -145,7 +145,6 @@ func main() {
|
||||
default:
|
||||
// Это уже что-то странное.
|
||||
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
17
types.go
17
types.go
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// Конфиг
|
||||
// myConfig прототип структурки с конфигом.
|
||||
type myConfig struct {
|
||||
Jabber struct {
|
||||
Server string `json:"server,omitempty"`
|
||||
@@ -32,15 +32,16 @@ type myConfig struct {
|
||||
NoEcho bool `json:"noecho,omitempty"`
|
||||
}
|
||||
|
||||
// Белый список
|
||||
// myWhiteList прототип структурки с белым списком jid-ов.
|
||||
type myWhiteList struct {
|
||||
Whitelist []struct {
|
||||
RoomName string `json:"room_name,omitempty"`
|
||||
Jid []string `json:"jid,omitempty"`
|
||||
WipeBans bool `json:"wipe_bans,omitempty"`
|
||||
} `json:"whitelist,omitampty"`
|
||||
} `json:"whitelist,omitempty"`
|
||||
}
|
||||
|
||||
// myBlackList прототип структурки с чёрным списком jid-ов.
|
||||
type myBlackList struct {
|
||||
Blacklist []struct {
|
||||
RoomName string `json:"room_name,omitempty"`
|
||||
@@ -48,6 +49,8 @@ type myBlackList struct {
|
||||
} `json:"blacklist,omitempty"`
|
||||
}
|
||||
|
||||
// jabberSimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например,
|
||||
// https://xmpp.org/extensions/xep-0030.html#example-18 .
|
||||
type jabberSimpleIqGetQuery struct {
|
||||
XMLName xml.Name `xml:"query"`
|
||||
Text string `xml:",chardata"`
|
||||
@@ -55,6 +58,7 @@ type jabberSimpleIqGetQuery struct {
|
||||
Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items"
|
||||
}
|
||||
|
||||
// jabberPubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub.
|
||||
type jabberPubsubIQGetQuery struct {
|
||||
XMLName xml.Name `xml:"pubsub"`
|
||||
Text string `xml:",chardata"`
|
||||
@@ -66,6 +70,8 @@ type jabberPubsubIQGetQuery struct {
|
||||
} `xml:"items"`
|
||||
}
|
||||
|
||||
// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
|
||||
// https://xmpp.org/extensions/xep-0202.html
|
||||
type jabberTimeIqGetQuery struct {
|
||||
// <time xmlns="urn:xmpp:time"/>
|
||||
XMLName xml.Name `xml:"time"`
|
||||
@@ -73,13 +79,16 @@ type jabberTimeIqGetQuery struct {
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
}
|
||||
|
||||
// jabberIqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
|
||||
type jabberIqPing struct {
|
||||
XMLName xml.Name `xml:"ping"`
|
||||
Text string `xml:",chardata"`
|
||||
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"`
|
||||
Text string `xml:",chardata"`
|
||||
Type string `xml:"type,attr"`
|
||||
|
||||
6
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
6
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
@@ -664,7 +664,7 @@ func (c *Client) init(o *Options) error {
|
||||
c.domain = domain
|
||||
|
||||
if o.Session {
|
||||
//if server support session, open it
|
||||
// if server support session, open it
|
||||
fmt.Fprintf(StanzaWriter, "<iq to='%s' type='set' id='%x'><session xmlns='%s'/></iq>", xmlEscape(domain), cookie, nsSession)
|
||||
}
|
||||
|
||||
@@ -700,7 +700,7 @@ func (c *Client) startTLSIfRequired(f *streamFeatures, o *Options, domain string
|
||||
tc := o.TLSConfig
|
||||
if tc == nil {
|
||||
tc = DefaultConfig.Clone()
|
||||
//TODO(scott): we should consider using the server's address or reverse lookup
|
||||
// TODO(scott): we should consider using the server's address or reverse lookup
|
||||
tc.ServerName = domain
|
||||
}
|
||||
t := tls.Client(c.conn, tc)
|
||||
@@ -796,7 +796,7 @@ type Presence struct {
|
||||
Status string
|
||||
Priority string
|
||||
ID string
|
||||
Affilation string
|
||||
Affiliation string
|
||||
Role 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.
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
// elements equal. If the lengths are different, Equal returns false.
|
||||
// Otherwise, the elements are compared in increasing index order, and the
|
||||
// comparison stops at the first unequal pair.
|
||||
// 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) {
|
||||
return false
|
||||
}
|
||||
@@ -31,12 +28,12 @@ func Equal[E comparable](s1, s2 []E) bool {
|
||||
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,
|
||||
// EqualFunc returns false. Otherwise, the elements are compared in
|
||||
// increasing index order, and the comparison stops at the first index
|
||||
// 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) {
|
||||
return false
|
||||
}
|
||||
@@ -49,45 +46,37 @@ func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare compares the elements of s1 and s2.
|
||||
// The elements are compared sequentially, starting at index 0,
|
||||
// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair
|
||||
// of elements. The elements are compared sequentially, starting at index 0,
|
||||
// until one element is not equal to the other.
|
||||
// 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
|
||||
// considered less than the longer one.
|
||||
// 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[E constraints.Ordered](s1, s2 []E) int {
|
||||
s2len := len(s2)
|
||||
func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int {
|
||||
for i, v1 := range s1 {
|
||||
if i >= s2len {
|
||||
if i >= len(s2) {
|
||||
return +1
|
||||
}
|
||||
v2 := s2[i]
|
||||
switch {
|
||||
case v1 < v2:
|
||||
return -1
|
||||
case v1 > v2:
|
||||
return +1
|
||||
if c := cmpCompare(v1, v2); c != 0 {
|
||||
return c
|
||||
}
|
||||
}
|
||||
if len(s1) < s2len {
|
||||
if len(s1) < len(s2) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// CompareFunc is like Compare but uses a comparison function
|
||||
// on each pair of elements. The elements are compared in increasing
|
||||
// index order, and the comparisons stop after the first time cmp
|
||||
// returns non-zero.
|
||||
// CompareFunc is like [Compare] but uses a custom comparison function on each
|
||||
// pair of elements.
|
||||
// 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),
|
||||
// and +1 if len(s1) > len(s2).
|
||||
func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
|
||||
s2len := len(s2)
|
||||
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int {
|
||||
for i, v1 := range s1 {
|
||||
if i >= s2len {
|
||||
if i >= len(s2) {
|
||||
return +1
|
||||
}
|
||||
v2 := s2[i]
|
||||
@@ -95,7 +84,7 @@ func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
|
||||
return c
|
||||
}
|
||||
}
|
||||
if len(s1) < s2len {
|
||||
if len(s1) < len(s2) {
|
||||
return -1
|
||||
}
|
||||
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,
|
||||
// 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 {
|
||||
if v == s[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]),
|
||||
// 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 {
|
||||
if f(s[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.
|
||||
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
|
||||
}
|
||||
|
||||
// ContainsFunc reports whether at least one
|
||||
// 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
|
||||
}
|
||||
|
||||
// Insert inserts the values v... into s at index i,
|
||||
// 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.
|
||||
// This function is O(len(s) + len(v)).
|
||||
func Insert[S ~[]E, E any](s S, i int, v ...E) S {
|
||||
tot := len(s) + len(v)
|
||||
if tot <= cap(s) {
|
||||
s2 := s[:tot]
|
||||
copy(s2[i+len(v):], s[i:])
|
||||
m := len(v)
|
||||
if m == 0 {
|
||||
return s
|
||||
}
|
||||
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+m:], s[i:])
|
||||
return s2
|
||||
}
|
||||
s2 := make(S, tot)
|
||||
copy(s2, s[:i])
|
||||
copy(s2[i:], v)
|
||||
copy(s2[i+len(v):], s[i:])
|
||||
return s2
|
||||
s = s[:n+m]
|
||||
|
||||
// before:
|
||||
// s: aaaaaaaabbbbccccccccdddd
|
||||
// ^ ^ ^ ^
|
||||
// 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 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
|
||||
// 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
|
||||
@@ -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
|
||||
// collected.
|
||||
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
||||
// Don't start copying elements until we find one to delete.
|
||||
for i, v := range s {
|
||||
if del(v) {
|
||||
j := i
|
||||
for i++; i < len(s); i++ {
|
||||
v = s[i]
|
||||
if !del(v) {
|
||||
s[j] = v
|
||||
j++
|
||||
}
|
||||
}
|
||||
return s[:j]
|
||||
}
|
||||
}
|
||||
i := IndexFunc(s, del)
|
||||
if i == -1 {
|
||||
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
|
||||
// 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 {
|
||||
_ = 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:])
|
||||
if tot <= cap(s) {
|
||||
s2 := s[:tot]
|
||||
copy(s2[i+len(v):], s[j:])
|
||||
if tot > cap(s) {
|
||||
// Too big to fit, allocate and copy over.
|
||||
s2 := append(s[:i], make(S, tot-i)...) // See Insert
|
||||
copy(s2[i:], v)
|
||||
copy(s2[i+len(v):], s[j:])
|
||||
return s2
|
||||
}
|
||||
s2 := make(S, tot)
|
||||
copy(s2, s[:i])
|
||||
copy(s2[i:], v)
|
||||
copy(s2[i+len(v):], s[j:])
|
||||
return s2
|
||||
|
||||
r := s[:tot]
|
||||
|
||||
if i+len(v) <= j {
|
||||
// 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.
|
||||
@@ -222,7 +343,8 @@ func Clone[S ~[]E, E any](s S) S {
|
||||
|
||||
// Compact replaces consecutive runs of equal elements with a single copy.
|
||||
// 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
|
||||
// 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.
|
||||
@@ -242,7 +364,8 @@ func Compact[S ~[]E, E comparable](s S) S {
|
||||
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 {
|
||||
if len(s) < 2 {
|
||||
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 {
|
||||
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
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -exp
|
||||
|
||||
package slices
|
||||
|
||||
import (
|
||||
@@ -11,57 +13,116 @@ import (
|
||||
)
|
||||
|
||||
// Sort sorts a slice of any ordered type in ascending order.
|
||||
// Sort may fail to sort correctly when sorting slices of floating-point
|
||||
// numbers containing Not-a-number (NaN) values.
|
||||
// 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) {
|
||||
// When sorting floating-point numbers, NaNs are ordered before other values.
|
||||
func Sort[S ~[]E, E constraints.Ordered](x S) {
|
||||
n := len(x)
|
||||
pdqsortOrdered(x, 0, n, bits.Len(uint(n)))
|
||||
}
|
||||
|
||||
// SortFunc sorts the slice x in ascending order as determined by the less function.
|
||||
// This sort is not guaranteed to be stable.
|
||||
// SortFunc sorts the slice x in ascending order as determined by the cmp
|
||||
// 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.
|
||||
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)
|
||||
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
|
||||
// elements, using less to compare elements.
|
||||
func SortStableFunc[E any](x []E, less func(a, b E) bool) {
|
||||
stableLessFunc(x, len(x), less)
|
||||
// elements, using cmp to compare elements in the same way as [SortFunc].
|
||||
func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
||||
stableCmpFunc(x, len(x), cmp)
|
||||
}
|
||||
|
||||
// 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-- {
|
||||
if x[i] < x[i-1] {
|
||||
if cmpLess(x[i], x[i-1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSortedFunc reports whether x is sorted in ascending order, with less as the
|
||||
// comparison function.
|
||||
func IsSortedFunc[E any](x []E, less func(a, b E) bool) bool {
|
||||
// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the
|
||||
// comparison function as defined by [SortFunc].
|
||||
func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool {
|
||||
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 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
|
||||
// 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
|
||||
// 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.
|
||||
n := len(x)
|
||||
// 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 {
|
||||
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
||||
// i ≤ h < j
|
||||
if x[h] < target {
|
||||
if cmpLess(x[h], target) {
|
||||
i = h + 1 // preserves x[i-1] < target
|
||||
} else {
|
||||
j = h // preserves x[j] >= target
|
||||
}
|
||||
}
|
||||
// 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"
|
||||
// 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,
|
||||
// or a positive number if the slice element follows the target.
|
||||
// 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.
|
||||
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)
|
||||
// Define cmp(x[-1], target) < 0 and cmp(x[n], 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 {
|
||||
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
|
||||
|
||||
// insertionSortLessFunc sorts data[a:b] using insertion sort.
|
||||
func insertionSortLessFunc[E any](data []E, a, b int, less func(a, b E) bool) {
|
||||
// insertionSortCmpFunc sorts data[a:b] using insertion sort.
|
||||
func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
for {
|
||||
child := 2*root + 1
|
||||
if child >= hi {
|
||||
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++
|
||||
}
|
||||
if !less(data[first+root], data[first+child]) {
|
||||
if !(cmp(data[first+root], data[first+child]) < 0) {
|
||||
return
|
||||
}
|
||||
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
|
||||
lo := 0
|
||||
hi := b - a
|
||||
|
||||
// Build heap with greatest element at top.
|
||||
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.
|
||||
for i := hi - 1; i >= 0; i-- {
|
||||
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.
|
||||
// pdqsort paper: https://arxiv.org/pdf/2106.05123.pdf
|
||||
// C++ implementation: https://github.com/orlp/pdqsort
|
||||
// Rust implementation: https://docs.rs/pdqsort/latest/pdqsort/
|
||||
// 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
|
||||
|
||||
var (
|
||||
@@ -70,25 +70,25 @@ func pdqsortLessFunc[E any](data []E, a, b, limit int, less func(a, b E) bool) {
|
||||
length := b - a
|
||||
|
||||
if length <= maxInsertion {
|
||||
insertionSortLessFunc(data, a, b, less)
|
||||
insertionSortCmpFunc(data, a, b, cmp)
|
||||
return
|
||||
}
|
||||
|
||||
// Fall back to heapsort if too many bad choices were made.
|
||||
if limit == 0 {
|
||||
heapSortLessFunc(data, a, b, less)
|
||||
heapSortCmpFunc(data, a, b, cmp)
|
||||
return
|
||||
}
|
||||
|
||||
// If the last partitioning was imbalanced, we need to breaking patterns.
|
||||
if !wasBalanced {
|
||||
breakPatternsLessFunc(data, a, b, less)
|
||||
breakPatternsCmpFunc(data, a, b, cmp)
|
||||
limit--
|
||||
}
|
||||
|
||||
pivot, hint := choosePivotLessFunc(data, a, b, less)
|
||||
pivot, hint := choosePivotCmpFunc(data, a, b, cmp)
|
||||
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.
|
||||
// After reversing it is pivot-a elements before the end of the array.
|
||||
// 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.
|
||||
if wasBalanced && wasPartitioned && hint == increasingHint {
|
||||
if partialInsertionSortLessFunc(data, a, b, less) {
|
||||
if partialInsertionSortCmpFunc(data, a, b, cmp) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Probably the slice contains many duplicate elements, partition the slice into
|
||||
// elements equal to and elements greater than the pivot.
|
||||
if a > 0 && !less(data[a-1], data[pivot]) {
|
||||
mid := partitionEqualLessFunc(data, a, b, pivot, less)
|
||||
if a > 0 && !(cmp(data[a-1], data[pivot]) < 0) {
|
||||
mid := partitionEqualCmpFunc(data, a, b, pivot, cmp)
|
||||
a = mid
|
||||
continue
|
||||
}
|
||||
|
||||
mid, alreadyPartitioned := partitionLessFunc(data, a, b, pivot, less)
|
||||
mid, alreadyPartitioned := partitionCmpFunc(data, a, b, pivot, cmp)
|
||||
wasPartitioned = alreadyPartitioned
|
||||
|
||||
leftLen, rightLen := mid-a, b-mid
|
||||
balanceThreshold := length / 8
|
||||
if leftLen < rightLen {
|
||||
wasBalanced = leftLen >= balanceThreshold
|
||||
pdqsortLessFunc(data, a, mid, limit, less)
|
||||
pdqsortCmpFunc(data, a, mid, limit, cmp)
|
||||
a = mid + 1
|
||||
} else {
|
||||
wasBalanced = rightLen >= balanceThreshold
|
||||
pdqsortLessFunc(data, mid+1, b, limit, less)
|
||||
pdqsortCmpFunc(data, mid+1, b, limit, cmp)
|
||||
b = mid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// partitionLessFunc does one quicksort partition.
|
||||
// partitionCmpFunc does one quicksort partition.
|
||||
// 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.
|
||||
// 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]
|
||||
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++
|
||||
}
|
||||
for i <= j && !less(data[j], data[a]) {
|
||||
for i <= j && !(cmp(data[j], data[a]) < 0) {
|
||||
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--
|
||||
|
||||
for {
|
||||
for i <= j && less(data[i], data[a]) {
|
||||
for i <= j && (cmp(data[i], data[a]) < 0) {
|
||||
i++
|
||||
}
|
||||
for i <= j && !less(data[j], data[a]) {
|
||||
for i <= j && !(cmp(data[j], data[a]) < 0) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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].
|
||||
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]
|
||||
i, j := a+1, b-1 // i and j are inclusive of the elements remaining to be partitioned
|
||||
|
||||
for {
|
||||
for i <= j && !less(data[a], data[i]) {
|
||||
for i <= j && !(cmp(data[a], data[i]) < 0) {
|
||||
i++
|
||||
}
|
||||
for i <= j && less(data[a], data[j]) {
|
||||
for i <= j && (cmp(data[a], data[j]) < 0) {
|
||||
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
|
||||
}
|
||||
|
||||
// partialInsertionSortLessFunc 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 {
|
||||
// partialInsertionSortCmpFunc partially sorts a slice, returns true if the slice is sorted at the end.
|
||||
func partialInsertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) bool {
|
||||
const (
|
||||
maxSteps = 5 // maximum number of adjacent out-of-order pairs that will get shifted
|
||||
shortestShifting = 50 // don't shift any elements on short arrays
|
||||
)
|
||||
i := a + 1
|
||||
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++
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
if i-a >= 2 {
|
||||
for j := i - 1; j >= 1; j-- {
|
||||
if !less(data[j], data[j-1]) {
|
||||
if !(cmp(data[j], data[j-1]) < 0) {
|
||||
break
|
||||
}
|
||||
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.
|
||||
if b-i >= 2 {
|
||||
for j := i + 1; j < b; j++ {
|
||||
if !less(data[j], data[j-1]) {
|
||||
if !(cmp(data[j], data[j-1]) < 0) {
|
||||
break
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
if length >= 8 {
|
||||
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.
|
||||
// [8,shortestNinther): uses the simple median-of-three 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 (
|
||||
shortestNinther = 50
|
||||
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 >= shortestNinther {
|
||||
// Tukey ninther method, the idea came from Rust's implementation.
|
||||
i = medianAdjacentLessFunc(data, i, &swaps, less)
|
||||
j = medianAdjacentLessFunc(data, j, &swaps, less)
|
||||
k = medianAdjacentLessFunc(data, k, &swaps, less)
|
||||
i = medianAdjacentCmpFunc(data, i, &swaps, cmp)
|
||||
j = medianAdjacentCmpFunc(data, j, &swaps, cmp)
|
||||
k = medianAdjacentCmpFunc(data, k, &swaps, cmp)
|
||||
}
|
||||
// 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 {
|
||||
@@ -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.
|
||||
func order2LessFunc[E any](data []E, a, b int, swaps *int, less func(a, b E) bool) (int, int) {
|
||||
if less(data[b], data[a]) {
|
||||
// order2CmpFunc returns x,y where data[x] <= data[y], where x,y=a,b or x,y=b,a.
|
||||
func order2CmpFunc[E any](data []E, a, b int, swaps *int, cmp func(a, b E) int) (int, int) {
|
||||
if cmp(data[b], data[a]) < 0 {
|
||||
*swaps++
|
||||
return b, a
|
||||
}
|
||||
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.
|
||||
func medianLessFunc[E any](data []E, a, b, c int, swaps *int, less func(a, b E) bool) int {
|
||||
a, b = order2LessFunc(data, a, b, swaps, less)
|
||||
b, c = order2LessFunc(data, b, c, swaps, less)
|
||||
a, b = order2LessFunc(data, a, b, swaps, less)
|
||||
// medianCmpFunc returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c.
|
||||
func medianCmpFunc[E any](data []E, a, b, c int, swaps *int, cmp func(a, b E) int) int {
|
||||
a, b = order2CmpFunc(data, a, b, swaps, cmp)
|
||||
b, c = order2CmpFunc(data, b, c, swaps, cmp)
|
||||
a, b = order2CmpFunc(data, a, b, swaps, cmp)
|
||||
return b
|
||||
}
|
||||
|
||||
// medianAdjacentLessFunc 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 {
|
||||
return medianLessFunc(data, a-1, a, a+1, swaps, less)
|
||||
// medianAdjacentCmpFunc finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a.
|
||||
func medianAdjacentCmpFunc[E any](data []E, a int, swaps *int, cmp func(a, b E) int) int {
|
||||
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
|
||||
j := b - 1
|
||||
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++ {
|
||||
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
|
||||
a, b := 0, blockSize
|
||||
for b <= n {
|
||||
insertionSortLessFunc(data, a, b, less)
|
||||
insertionSortCmpFunc(data, a, b, cmp)
|
||||
a = b
|
||||
b += blockSize
|
||||
}
|
||||
insertionSortLessFunc(data, a, n, less)
|
||||
insertionSortCmpFunc(data, a, n, cmp)
|
||||
|
||||
for blockSize < n {
|
||||
a, b = 0, 2*blockSize
|
||||
for b <= n {
|
||||
symMergeLessFunc(data, a, a+blockSize, b, less)
|
||||
symMergeCmpFunc(data, a, a+blockSize, b, cmp)
|
||||
a = b
|
||||
b += 2 * blockSize
|
||||
}
|
||||
if m := a + blockSize; m < n {
|
||||
symMergeLessFunc(data, a, m, n, less)
|
||||
symMergeCmpFunc(data, a, m, n, cmp)
|
||||
}
|
||||
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
|
||||
// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz
|
||||
// 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.
|
||||
// Having the caller check this condition eliminates many leaf recursion calls,
|
||||
// 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
|
||||
// by direct insertion of data[a] into data[m:b]
|
||||
// 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
|
||||
for i < j {
|
||||
h := int(uint(i+j) >> 1)
|
||||
if less(data[h], data[a]) {
|
||||
if cmp(data[h], data[a]) < 0 {
|
||||
i = h + 1
|
||||
} else {
|
||||
j = h
|
||||
@@ -411,7 +411,7 @@ func symMergeLessFunc[E any](data []E, a, m, b int, less func(a, b E) bool) {
|
||||
j := m
|
||||
for i < j {
|
||||
h := int(uint(i+j) >> 1)
|
||||
if !less(data[m], data[h]) {
|
||||
if !(cmp(data[m], data[h]) < 0) {
|
||||
i = h + 1
|
||||
} else {
|
||||
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 {
|
||||
c := int(uint(start+r) >> 1)
|
||||
if !less(data[p-c], data[c]) {
|
||||
if !(cmp(data[p-c], data[c]) < 0) {
|
||||
start = c + 1
|
||||
} else {
|
||||
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
|
||||
if start < m && m < end {
|
||||
rotateLessFunc(data, start, m, end, less)
|
||||
rotateCmpFunc(data, start, m, end, cmp)
|
||||
}
|
||||
if a < start && start < mid {
|
||||
symMergeLessFunc(data, a, start, mid, less)
|
||||
symMergeCmpFunc(data, a, start, mid, cmp)
|
||||
}
|
||||
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'.
|
||||
// rotate performs at most b-a many calls to data.Swap,
|
||||
// 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
|
||||
j := b - m
|
||||
|
||||
for i != j {
|
||||
if i > j {
|
||||
swapRangeLessFunc(data, m-i, m, j, less)
|
||||
swapRangeCmpFunc(data, m-i, m, j, cmp)
|
||||
i -= j
|
||||
} else {
|
||||
swapRangeLessFunc(data, m-i, m+j-i, i, less)
|
||||
swapRangeCmpFunc(data, m-i, m+j-i, i, cmp)
|
||||
j -= i
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
func insertionSortOrdered[E constraints.Ordered](data []E, a, b int) {
|
||||
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]
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@ func siftDownOrdered[E constraints.Ordered](data []E, lo, hi, first int) {
|
||||
if child >= hi {
|
||||
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++
|
||||
}
|
||||
if !(data[first+root] < data[first+child]) {
|
||||
if !cmpLess(data[first+root], data[first+child]) {
|
||||
return
|
||||
}
|
||||
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
|
||||
// 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)
|
||||
a = mid
|
||||
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]
|
||||
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++
|
||||
}
|
||||
for i <= j && !(data[j] < data[a]) {
|
||||
for i <= j && !cmpLess(data[j], data[a]) {
|
||||
j--
|
||||
}
|
||||
if i > j {
|
||||
@@ -153,10 +153,10 @@ func partitionOrdered[E constraints.Ordered](data []E, a, b, pivot int) (newpivo
|
||||
j--
|
||||
|
||||
for {
|
||||
for i <= j && (data[i] < data[a]) {
|
||||
for i <= j && cmpLess(data[i], data[a]) {
|
||||
i++
|
||||
}
|
||||
for i <= j && !(data[j] < data[a]) {
|
||||
for i <= j && !cmpLess(data[j], data[a]) {
|
||||
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
|
||||
|
||||
for {
|
||||
for i <= j && !(data[a] < data[i]) {
|
||||
for i <= j && !cmpLess(data[a], data[i]) {
|
||||
i++
|
||||
}
|
||||
for i <= j && (data[a] < data[j]) {
|
||||
for i <= j && cmpLess(data[a], data[j]) {
|
||||
j--
|
||||
}
|
||||
if i > j {
|
||||
@@ -201,7 +201,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
|
||||
)
|
||||
i := a + 1
|
||||
for j := 0; j < maxSteps; j++ {
|
||||
for i < b && !(data[i] < data[i-1]) {
|
||||
for i < b && !cmpLess(data[i], data[i-1]) {
|
||||
i++
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b int) bool
|
||||
// Shift the smaller one to the left.
|
||||
if i-a >= 2 {
|
||||
for j := i - 1; j >= 1; j-- {
|
||||
if !(data[j] < data[j-1]) {
|
||||
if !cmpLess(data[j], data[j-1]) {
|
||||
break
|
||||
}
|
||||
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.
|
||||
if b-i >= 2 {
|
||||
for j := i + 1; j < b; j++ {
|
||||
if !(data[j] < data[j-1]) {
|
||||
if !cmpLess(data[j], data[j-1]) {
|
||||
break
|
||||
}
|
||||
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.
|
||||
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++
|
||||
return b, a
|
||||
}
|
||||
@@ -389,7 +389,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
||||
j := b
|
||||
for i < j {
|
||||
h := int(uint(i+j) >> 1)
|
||||
if data[h] < data[a] {
|
||||
if cmpLess(data[h], data[a]) {
|
||||
i = h + 1
|
||||
} else {
|
||||
j = h
|
||||
@@ -413,7 +413,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
||||
j := m
|
||||
for i < j {
|
||||
h := int(uint(i+j) >> 1)
|
||||
if !(data[m] < data[h]) {
|
||||
if !cmpLess(data[m], data[h]) {
|
||||
i = h + 1
|
||||
} else {
|
||||
j = h
|
||||
@@ -440,7 +440,7 @@ func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
||||
|
||||
for start < r {
|
||||
c := int(uint(start+r) >> 1)
|
||||
if !(data[p-c] < data[c]) {
|
||||
if !cmpLess(data[p-c], data[c]) {
|
||||
start = c + 1
|
||||
} else {
|
||||
r = c
|
||||
|
||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
@@ -1,7 +1,7 @@
|
||||
# github.com/davecgh/go-spew v1.1.1
|
||||
## explicit
|
||||
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
|
||||
github.com/eleksir/go-xmpp
|
||||
# github.com/hjson/hjson-go v3.3.0+incompatible
|
||||
@@ -13,7 +13,7 @@ github.com/sirupsen/logrus
|
||||
# golang.org/x/crypto v0.11.0
|
||||
## explicit; go 1.17
|
||||
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
|
||||
golang.org/x/exp/constraints
|
||||
golang.org/x/exp/slices
|
||||
|
||||
Reference in New Issue
Block a user