Compare commits
48 Commits
39ac09f82a
...
v0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ebd2b48d1 | ||
|
|
0ed74ed812 | ||
|
|
f9dd1acc45 | ||
|
|
21b5f59c1c | ||
|
|
073138bde4 | ||
|
|
fb19f8056a | ||
|
|
678d830f8c | ||
|
|
17fe947df9 | ||
|
|
0264e675a4 | ||
|
|
27f79905f4 | ||
|
|
0773c29359 | ||
|
|
c803682b8b | ||
|
|
9a9516e80d | ||
|
|
ea6e562928 | ||
|
|
420ed7d932 | ||
|
|
b690b54891 | ||
|
|
40c574faf7 | ||
|
|
31490b0922 | ||
|
|
d88f08479d | ||
|
|
139ede7022 | ||
|
|
2bf0ac18b9 | ||
|
|
a08baa5f38 | ||
|
|
122f16c408 | ||
|
|
07da3e5a49 | ||
|
|
a9935abf53 | ||
|
|
75b9ab8a8b | ||
|
|
e48be3936e | ||
|
|
c5f011dabc | ||
|
|
b126052cb6 | ||
|
|
637d7778e3 | ||
|
|
ddd5e46dbf | ||
|
|
5b730c9684 | ||
|
|
8a4a1ae6b7 | ||
|
|
453c15c046 | ||
|
|
8de8d56d67 | ||
|
|
80dcfa7294 | ||
|
|
998a827d4b | ||
|
|
48541ce14d | ||
|
|
ca384b5165 | ||
|
|
91eac5cb6d | ||
|
|
716fc59700 | ||
|
|
7f609ffb97 | ||
|
|
59b92f450d | ||
|
|
0e9fa089c0 | ||
|
|
5bb7dccde4 | ||
|
|
9c793d4b6e | ||
|
|
a0a403b272 | ||
|
|
ae7d0365e1 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,7 +5,8 @@
|
|||||||
*.tmp
|
*.tmp
|
||||||
**/*.bak
|
**/*.bak
|
||||||
**/*.tmp
|
**/*.tmp
|
||||||
data/config.json
|
/data/config.json
|
||||||
data/whitelist.json
|
/data/whitelist.json
|
||||||
data/blacklist.json
|
/data/blacklist.json
|
||||||
buny-jabber-bot
|
/buny-jabber-bot
|
||||||
|
/buny-jabber-bot.exe
|
||||||
|
|||||||
69
.golangci.yml
Normal file
69
.golangci.yml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
modules-download-mode: readonly
|
||||||
|
allow-parallel-runners: true
|
||||||
|
go: '1.24'
|
||||||
|
issues:
|
||||||
|
exclude-dirs:
|
||||||
|
- data
|
||||||
|
- vendor
|
||||||
|
output:
|
||||||
|
sort-results: true
|
||||||
|
linters-settings:
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
# Оно не используется для повышения безопасности или каких-то таких секъюрых вещей, оно используется для выборки
|
||||||
|
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
|
||||||
|
- G404 # Insecure random number source (rand)
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- contextcheck
|
||||||
|
- decorder
|
||||||
|
- durationcheck
|
||||||
|
- errchkjson
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- exhaustruct
|
||||||
|
- copyloopvar
|
||||||
|
- ginkgolinter
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
- gocritic
|
||||||
|
- godot
|
||||||
|
- err113
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosmopolitan
|
||||||
|
- grouper
|
||||||
|
- importas
|
||||||
|
- ireturn
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- nilnil
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nonamedreturns
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- reassign
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- stylecheck
|
||||||
|
- unconvert
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- wsl
|
||||||
24
Design.txt
24
Design.txt
@@ -1,12 +1,5 @@
|
|||||||
Oсобенности реализации клиентской части.
|
Oсобенности реализации клиентской части.
|
||||||
|
|
||||||
Общие положения
|
|
||||||
------------------------------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Nick для room-ов един и не может задаваться на per-conference основе. На этом завязаны отсылка ping-ов и определение
|
|
||||||
pong-ов, как минимум. Хотя формально джойниться можно с любым ником :) но что-то не хочется потом распутывать этот
|
|
||||||
клубок.
|
|
||||||
|
|
||||||
|
|
||||||
XEP-0410 MUC Self-Ping (Schrödinger's Chat)
|
XEP-0410 MUC Self-Ping (Schrödinger's Chat)
|
||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -61,3 +54,20 @@ Cуществует несколько видов пинга:
|
|||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
Причину бана можно только включить или выключить в конфиге на per-chat основе. Фразу настроить невозможно. (не хотелось
|
Причину бана можно только включить или выключить в конфиге на per-chat основе. Фразу настроить невозможно. (не хотелось
|
||||||
реализовывать парсинг темплейтов, как минимум даты-времени)
|
реализовывать парсинг темплейтов, как минимум даты-времени)
|
||||||
|
|
||||||
|
|
||||||
|
Бан за фразы
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
Бот не читает историю, когда присоединяется к чатам, поэтому если его какое-то время не было в комнате и кто-то произнёс
|
||||||
|
непотребную фразу, ретроспективного бана не будет.
|
||||||
|
|
||||||
|
|
||||||
|
Проблема реконнекта
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
На текущий момент решена не совсем чисто. Скорее всего, при реконнекте утекают ресурсы, связанные с jabber-соединением.
|
||||||
|
Проблема в том, что приходится пересоздавать основной "объект" со всеми его полями.
|
||||||
|
|
||||||
|
Кроме того, sig handler фактичеки не завершается, потому что в него не приходит ни одного сигнала и формально остаётся
|
||||||
|
висеть на channel-е навечно.
|
||||||
|
|
||||||
|
А ещё есть пингеры каналов и пингеры сервера. Их бы тоже завершать явным образом.
|
||||||
|
|||||||
76
Makefile
76
Makefile
@@ -1,20 +1,88 @@
|
|||||||
#!/usr/bin/env gmake -f
|
#!/usr/bin/env gmake -f
|
||||||
|
|
||||||
CGO_ENABLED=0
|
|
||||||
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
||||||
|
MYNAME=buny-jabber-bot
|
||||||
|
BINARY=$(MYNAME)
|
||||||
|
TEST1=xml-software-version-query
|
||||||
|
|
||||||
|
# На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe
|
||||||
|
# не нужен
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
ifeq ($(strip $(GOOS)),)
|
||||||
|
BINARY=buny-jabber-bot.exe
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Если мы собираем бинарь под windows не на windows, то надо сделать бинарь с суффиксом .exe
|
||||||
|
ifeq ($(strip $(GOOS)),windows)
|
||||||
|
BINARY=buny-jabber-bot.exe
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Явно определяем символ новой строки, чтобы избежать неоднозначности на windows
|
||||||
|
define IFS
|
||||||
|
|
||||||
|
endef
|
||||||
|
|
||||||
|
## Используем классические таргеты, где первый встречаемый является таргетом по-умолчанию
|
||||||
all: clean build
|
all: clean build
|
||||||
|
|
||||||
|
## Этот таргет собирает бинарь
|
||||||
build:
|
build:
|
||||||
go build ${BUILDOPTS} -o "buny-jabber-bot" collection.go types.go globals.go lib.go event_parser.go buny.go main.go
|
# Ну и дальше просто билдим бинарник средствами гошки
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# вариант с powershell на windows
|
||||||
|
ifeq ($(SHELL),sh.exe)
|
||||||
|
SET CGO_ENABLED=0
|
||||||
|
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
|
else
|
||||||
|
# вариант с jetbrains golang на windows
|
||||||
|
CGO_ENABLED=0
|
||||||
|
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
|
endif
|
||||||
|
# вариант с bash/git (windows) и bash (linux)
|
||||||
|
else
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
## Удаляем бинарник средствами go
|
||||||
clean:
|
clean:
|
||||||
go clean
|
$(RM) ${BINARY}
|
||||||
|
|
||||||
|
|
||||||
|
## Служебный таргет, для целей разработки. Обновляет завендоренные либы, брутальным образом.
|
||||||
upgrade:
|
upgrade:
|
||||||
rm -rf vendor
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# вариант с jetbrains golang на windows или powershell на windows
|
||||||
|
ifeq ($(SHELL),sh.exe)
|
||||||
|
if exist vendor del /F /S /Q vendor >nul
|
||||||
|
# вариант с git/bash на windows
|
||||||
|
else
|
||||||
|
$(RM) -r vendor
|
||||||
|
endif
|
||||||
|
# вариант с bash на linux
|
||||||
|
else
|
||||||
|
$(RM) -r vendor
|
||||||
|
endif
|
||||||
go get -d -u -t ./...
|
go get -d -u -t ./...
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
||||||
|
test1:
|
||||||
|
# Ну и дальше просто билдим бинарник средствами гошки
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# вариант с powershell на windows
|
||||||
|
ifeq ($(SHELL),sh.exe)
|
||||||
|
SET CGO_ENABLED=0
|
||||||
|
go build ${BUILDOPTS} -o ${TEST1} ./cmd/${TEST1}.exe
|
||||||
|
else
|
||||||
|
# вариант с jetbrains golang на windows
|
||||||
|
CGO_ENABLED=0
|
||||||
|
go build ${BUILDOPTS} -o ${TEST1} ./cmd/${TEST1}.exe
|
||||||
|
endif
|
||||||
|
# вариант с bash/git (windows) и bash (linux)
|
||||||
|
else
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${TEST1} ./cmd/${TEST1}
|
||||||
|
endif
|
||||||
|
|
||||||
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -1,25 +1,43 @@
|
|||||||
# Buny Jabber Bot
|
# Buny Jabber Bot
|
||||||
|
|
||||||
Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата.
|
<table style="border-collapse: collapse; border: none;">
|
||||||
|
<tr style="border: none;">
|
||||||
Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, который следует себе задать перед его
|
<td rowspan=2 style="border: none;"><img src="assets/avatar.png" alt="Бани" style="max-width: none; min-width:100%"></td>
|
||||||
использованием - а не забаню ли я кого-то не того?
|
<td style="border: none;">Простой бот, работающий по протоколу xmpp. Его задача - банить абьюзеров чата. При его
|
||||||
|
создании не стояло задачи сделать что-то красивое в архитектурном или каком-то другом плане.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border: none;">
|
||||||
|
<td style="border: none;">Этот бот автоматизирует уборку человеческой грязи. Самый важный вопрос, который следует себе
|
||||||
|
задать перед его использованием - а не забаню ли я кого-то лишнего?
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Что он может?
|
## Что он может?
|
||||||
|
|
||||||
Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. (Пока что) Шифрование
|
* Имеет возможность соединяться с сервером по протоколу xmpp как с шифрованием, так и без. (Пока что) Шифрование
|
||||||
работает по классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде),
|
работает по классическим механикам (шифрованный канал связи, без возможности коммуникации в незашифрованном виде),
|
||||||
без поддержки механизма start tls. Что-то в гошке или на стороне сервера при выборе StartTLS не работает.
|
без поддержки механизма start tls. (Что-то в гошке или на стороне сервера при выборе StartTLS не работает.)
|
||||||
|
* Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам:
|
||||||
Имеет возможность заносить пользователей в бан-лист, согласно заданным в чёрном списке правилам.
|
- По совпадению с регулярными выражениями в nick-е или jid-е злодея
|
||||||
|
- По регулярным выражениям характерных фраз.
|
||||||
|
- По названию и/или версии и, если есть, ос злодея.
|
||||||
|
- Правила можно настроить как глобально, для всех комнат, где присутствует бот, так и для каждой комнаты отдельно.
|
||||||
|
- При изменении списка правил бота не надо перезапускать, достаточно отдать ему команду rehash либо в приват, либо
|
||||||
|
прям в чатике.
|
||||||
|
* Есть настройка заходить в разные комнаты под разными никами.
|
||||||
|
|
||||||
## Что он не может?
|
## Что он не может?
|
||||||
|
|
||||||
* Не может заходить в комнаты, защищённые паролем.
|
* Не может заходить в комнаты, защищённые паролем.
|
||||||
* Не умеет разгадывать капчу.
|
* Не умеет разгадывать капчу.
|
||||||
|
|
||||||
|
Предполагается, что если на комнату навешен пароль, то такой бот там не нужен, а капча защищает от злодеев на 100%,
|
||||||
|
иначе её навешивать бесполезно.
|
||||||
|
|
||||||
## Как заставить его работать?
|
## Как заставить его работать?
|
||||||
Технически, бот собирается гошкой 1.20 под платформу linux. Работоспособность на других платформах не тестировалась.
|
|
||||||
|
Технически, бот собирается гошкой 1.21 под платформу linux. Работоспособность на других платформах не тестировалась.
|
||||||
|
|
||||||
Чтобы получить готовые бинарники, достаточно выполнить команду:
|
Чтобы получить готовые бинарники, достаточно выполнить команду:
|
||||||
|
|
||||||
@@ -27,16 +45,20 @@
|
|||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
в итоге получится бинарник **buny-jabber-bot**, которому нужен конфиг data/config.json для работы.
|
в итоге получится бинарник **buny-jabber-bot**, которому нужен конфиг data/config.json, whitelist.json и blacklist.json
|
||||||
|
для работы.
|
||||||
|
|
||||||
А также понадобятся **data/blacklist.json** и **data/whitelist.json**, в которых находятся списки правил, злодеев и
|
В файлах **data/blacklist.json** и **data/whitelist.json** находятся списки злодеев и списки проверенных пользователей.
|
||||||
списки проверенных пользователей.
|
|
||||||
|
|
||||||
### Замечание
|
### Замечание
|
||||||
|
|
||||||
Очевидным образом в комнате банить может только админ. Поэтому боту придётся выдать админские права. А комнату сделать
|
Очевидным образом в комнате банить может только админ. Поэтому боту придётся выдать админские права. А комнату сделать
|
||||||
не анонимной (то есть, чтобы как минимум админы могли видеть реальный jid, потому что в бан отправляется именно он).
|
не анонимной (то есть, чтобы как минимум админы могли видеть реальный jid, потому что в бан отправляется именно он).
|
||||||
|
|
||||||
|
Почему нельзя оставить комнату анонимной? А тут всё просто, в банлист заносится реальный jid пользователя, а не его ник.
|
||||||
|
Соответственно, бот должен видеть этот самый реальный jid. В анонимной комнате бот не сможет увидеть реальный jid.
|
||||||
|
А автокик - это, конечно, хорошо, но есть нюанс: можно, например, гадить ником, поэтому бот в автокик не умеет.
|
||||||
|
|
||||||
## Как работает бан?
|
## Как работает бан?
|
||||||
|
|
||||||
Никакой магии, всё по стандарту, согласно [xep-0045](https://xmpp.org/extensions/xep-0045.html#ban).
|
Никакой магии, всё по стандарту, согласно [xep-0045](https://xmpp.org/extensions/xep-0045.html#ban).
|
||||||
@@ -45,28 +67,9 @@ make
|
|||||||
помещает этот jid в список изгнанных из комнаты (бан-лист конкретной комнаты, в которой присутствует бот) и выгоняет
|
помещает этот jid в список изгнанных из комнаты (бан-лист конкретной комнаты, в которой присутствует бот) и выгоняет
|
||||||
этого jid-а.
|
этого jid-а.
|
||||||
|
|
||||||
### В чём тонкость?
|
|
||||||
|
|
||||||
Ровно в том, что для исполнения действия, боту надо заметить presence участника, а presence посылается
|
|
||||||
при изменении состояния клиента. То есть, если jid уже был в комнате на момент захода бота в неё, то его не забанят
|
|
||||||
(во всяком случае моментально). Самый простой способ поместить таких персонажей в бан - просто кикнуть их. Ихний
|
|
||||||
affiliation сменится на none, бот заметит presence и, если jid подпадает под правила чёрного списка, то отправит этот
|
|
||||||
jid в бан.
|
|
||||||
|
|
||||||
Но, простите, ведь "по правилам", при заходе клиента в комнату ему присылается presence всех участников, чтобы он мог
|
|
||||||
построить локальный ростер комнаты и начать отслеживать изменения состояния участников конференции. Почему бы в этот
|
|
||||||
момент не банить злодеев?
|
|
||||||
|
|
||||||
Да, так делать можно, однако пока этого нет. Дело в том, что presence самого себя участнику приходит последним (после
|
|
||||||
presence-ов тех, кто находится в комнате на момент появления в ней бота), и, согласно этому событию, можно считать, что
|
|
||||||
клиент зашёл в комнату успешно и формально *только после этого*, клиент может что-то пытаться делать в этой комнате.
|
|
||||||
Соответственно, чтобы кого-то банить исходя из списка presense-ов, который прилетает на входе, необходимо реализовать
|
|
||||||
"одноразовую" очередь тех, кого имеет смысл забанить и разгрести её "когда будет можно". Эта задача факультативная,
|
|
||||||
возможно, она будет решена потом.
|
|
||||||
|
|
||||||
## Как работает белый список?
|
## Как работает белый список?
|
||||||
|
|
||||||
Белый список - это список jid-ов, которых банить нельзя ни при каких обстоятельствах. В нём всегда неявно присутсвует
|
Белый список - это список jid-ов, которых банить нельзя ни при каких обстоятельствах. В нём всегда неявно присутствует
|
||||||
сам бот.
|
сам бот.
|
||||||
|
|
||||||
### Можно ли почистить ban-list?
|
### Можно ли почистить ban-list?
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Конечно, очень печально, что приходится развивать свои навыки программизмов на программах с такой задачей. Но жизнь не
|
Конечно, очень печально, что приходится развивать свои навыки программизмов на программах с такой задачей. Но жизнь не
|
||||||
всегда радует своими проявлениями.
|
всегда радует своими проявлениями.
|
||||||
|
|
||||||
История бота довольно банальна: к нам в чятик стал набергать один товарищ и нести чушь про политоту. Если его банить
|
История бота довольно банальна: к нам в чятик стал набегать один товарищ и нести чушь про политоту. Если его банить
|
||||||
классическими средствами, он просто создавал новый аккаунт и снова набегал гадить.
|
классическими средствами, он просто создавал новый аккаунт и снова набегал гадить.
|
||||||
|
|
||||||
И таким образом родилась идея как-то автоматизировать его бан.
|
И таким образом родилась идея как-то автоматизировать его бан.
|
||||||
|
|||||||
BIN
assets/avatar.png
Normal file
BIN
assets/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
259
buny.go
259
buny.go
@@ -1,259 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/eleksir/go-xmpp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buny(v xmpp.Presence) error { //nolint:gocognit,gocyclo
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
|
|
||||||
// списке, заносим пидора в список outcast-ов
|
|
||||||
if v.JID != "" {
|
|
||||||
// На всякий случай: себя никогда не баним, явным образом
|
|
||||||
if v.JID == talk.JID() {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
room := strings.SplitN(v.From, "/", 2)[0]
|
|
||||||
|
|
||||||
if room == "" {
|
|
||||||
log.Infof("We got empty room field in presence event, which kinda strange: %s", spew.Sdump(v))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
goodJid := strings.SplitN(v.JID, "/", 2)[0]
|
|
||||||
|
|
||||||
// Обрабатываем правила белого списка
|
|
||||||
for _, good := range whiteList.Whitelist {
|
|
||||||
// Глобальный белый список
|
|
||||||
if good.RoomName == "" {
|
|
||||||
for _, person := range good.Jid {
|
|
||||||
if person == goodJid {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Белый список для конкретных каналов
|
|
||||||
if good.RoomName == room {
|
|
||||||
for _, person := range good.Jid {
|
|
||||||
if person == goodJid {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
evilJid := strings.SplitN(v.JID, "/", 2)[0]
|
|
||||||
evilNick := ""
|
|
||||||
evilNicks := strings.SplitN(v.From, "/", 2)
|
|
||||||
// Давайте-ка похэндлим нонсенс - когда у нас в строке нету разделителя
|
|
||||||
if len(evilNicks) > 1 {
|
|
||||||
evilNick = evilNicks[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обрабатываем правила чёрного списка
|
|
||||||
for _, cRoom := range roomsConnected {
|
|
||||||
if cRoom == room {
|
|
||||||
for _, bEntry := range blackList.Blacklist {
|
|
||||||
// Обработаем правила глобального чёрного списка
|
|
||||||
if bEntry.RoomName == "" {
|
|
||||||
for _, jidRegexp := range bEntry.JidRe {
|
|
||||||
re, err := regexp.Compile(jidRegexp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if re.MatchString(evilJid) {
|
|
||||||
if id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nickRegexp := range bEntry.NickRe {
|
|
||||||
re, err := regexp.Compile(nickRegexp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", nickRegexp)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if re.MatchString(evilNick) {
|
|
||||||
// Баним именно jid
|
|
||||||
if id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработаем правила конкретного канала, комнаты конференции
|
|
||||||
if bEntry.RoomName == room {
|
|
||||||
for _, jidRegexp := range bEntry.JidRe {
|
|
||||||
re, err := regexp.Compile(jidRegexp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", jidRegexp)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if re.MatchString(evilJid) {
|
|
||||||
if id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bEntry.RoomName == room {
|
|
||||||
for _, nickRegexp := range bEntry.NickRe {
|
|
||||||
re, err := regexp.Compile(nickRegexp)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Incorrect regexp in blacklist: %s, skipping", nickRegexp)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if re.MatchString(evilNick) {
|
|
||||||
// Баним именно jid
|
|
||||||
if id, err := squash(room, evilJid, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// squash банит указанный jid в указанной комнате.
|
|
||||||
// reasonEnable указывает, надо ли писать дату автобана в банлисте в поле reason (это единственная причина, в которую
|
|
||||||
// умеет бот).
|
|
||||||
func squash(room, jid string, reasonEnable bool, vType string) (string, error) {
|
|
||||||
var (
|
|
||||||
id string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Infof("Hammer falls on %s", jid)
|
|
||||||
|
|
||||||
if config.Jabber.BanPhrasesEnable {
|
|
||||||
phrase := randomPhrase(config.Jabber.BanPhrases)
|
|
||||||
|
|
||||||
if _, err = talk.Send(
|
|
||||||
xmpp.Chat{ //nolint:exhaustruct
|
|
||||||
Remote: room,
|
|
||||||
Text: phrase,
|
|
||||||
Type: vType,
|
|
||||||
},
|
|
||||||
); err != nil {
|
|
||||||
err = fmt.Errorf("unable to send phrase to room %s: %w", room, err)
|
|
||||||
|
|
||||||
// Здесь возвращаем nil, т.к. за нас ошибку залоггирует код выше
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://xmpp.org/extensions/xep-0045.html#ban баним вот таким сообщением
|
|
||||||
ban := "<item affiliation='outcast' jid='" + jid + "'>"
|
|
||||||
|
|
||||||
if reasonEnable {
|
|
||||||
var t = time.Now()
|
|
||||||
ban += fmt.Sprintf(
|
|
||||||
"<reason>autoban at %04d.%02d.%02d %02d:%02d:%02d</reason>",
|
|
||||||
t.Year(),
|
|
||||||
t.Month(),
|
|
||||||
t.Day(),
|
|
||||||
t.Hour(),
|
|
||||||
t.Minute(),
|
|
||||||
t.Second(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ban += "<reason />"
|
|
||||||
}
|
|
||||||
|
|
||||||
ban += "</item>"
|
|
||||||
|
|
||||||
if id, err = talk.RawInformationQuery(
|
|
||||||
talk.JID(),
|
|
||||||
room,
|
|
||||||
"ban1",
|
|
||||||
xmpp.IQTypeSet,
|
|
||||||
"http://jabber.org/protocol/muc#admin",
|
|
||||||
ban,
|
|
||||||
); err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
162
cmd/buny-jabber-bot/main.go
Normal file
162
cmd/buny-jabber-bot/main.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"buny-jabber-bot/internal/jabber"
|
||||||
|
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/tomb.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed version
|
||||||
|
var version string
|
||||||
|
|
||||||
|
// main - фактичеcки, начало и основное тело программы.
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
var j = jabber.Jabber{ //nolint:exhaustruct
|
||||||
|
SigChan: make(chan os.Signal, 1),
|
||||||
|
GTomb: tomb.Tomb{},
|
||||||
|
RoomsConnected: make([]string, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
log.SetFormatter(&log.TextFormatter{ //nolint:exhaustruct
|
||||||
|
DisableQuote: true,
|
||||||
|
DisableLevelTruncation: false,
|
||||||
|
DisableColors: true,
|
||||||
|
FullTimestamp: true,
|
||||||
|
TimestampFormat: "2006-01-02 15:04:05",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := j.ReadConfig(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.C.Version = version
|
||||||
|
|
||||||
|
// no panic
|
||||||
|
switch j.C.Loglevel {
|
||||||
|
case "fatal":
|
||||||
|
log.SetLevel(log.FatalLevel)
|
||||||
|
case "error":
|
||||||
|
log.SetLevel(log.ErrorLevel)
|
||||||
|
case "warn":
|
||||||
|
log.SetLevel(log.WarnLevel)
|
||||||
|
case "info":
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
case "debug":
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
case "trace":
|
||||||
|
log.SetLevel(log.TraceLevel)
|
||||||
|
default:
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Откроем лог и скормим его логгеру.
|
||||||
|
if j.C.Log != "" {
|
||||||
|
logfile, err := os.OpenFile(j.C.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to open log file %s: %s", j.C.Log, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.SetOutput(logfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.C.ExeName, err = os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("Unable to find my executable: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle pricky windows case. But honestly i do not expect it runs on windows.
|
||||||
|
if regexp.MustCompile(".[Ee][Xx][Ee]$").MatchString(j.C.ExeName) {
|
||||||
|
j.C.ExeName = j.C.ExeName[:len(j.C.ExeName)-4]
|
||||||
|
|
||||||
|
if j.C.ExeName == "" {
|
||||||
|
j.C.ExeName = "buny-jabber-bot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Service %s v%s starting", j.C.ExeName, j.C.Version)
|
||||||
|
|
||||||
|
myLogLevel := log.GetLevel()
|
||||||
|
log.Warnf("Loglevel set to %v", myLogLevel)
|
||||||
|
|
||||||
|
verboseClient := false
|
||||||
|
|
||||||
|
if myLogLevel == log.TraceLevel {
|
||||||
|
verboseClient = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Самое время вгрузить белые и чёрные списки, если что - получим ошибку в лог.
|
||||||
|
if err := j.ReadWhitelist(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.ReadBlacklist(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace.
|
||||||
|
xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel)
|
||||||
|
|
||||||
|
// Хэндлер сигналов.
|
||||||
|
j.GTomb.Go(func() error { return j.SigHandler() }) //nolint: gocritic
|
||||||
|
|
||||||
|
signal.Notify(j.SigChan, os.Interrupt)
|
||||||
|
|
||||||
|
j.Options = &xmpp.Options{ //nolint:exhaustruct
|
||||||
|
Host: fmt.Sprintf("%s:%d", j.C.Jabber.Server, j.C.Jabber.Port),
|
||||||
|
User: j.C.Jabber.User,
|
||||||
|
Password: j.C.Jabber.Password,
|
||||||
|
Resource: j.C.Jabber.Resource,
|
||||||
|
NoTLS: !j.C.Jabber.Ssl,
|
||||||
|
StartTLS: j.C.Jabber.StartTLS,
|
||||||
|
TLSConfig: &tls.Config{ //nolint:exhaustruct
|
||||||
|
ServerName: j.C.Jabber.Server,
|
||||||
|
InsecureSkipVerify: !j.C.Jabber.SslVerify, //nolint:gosec
|
||||||
|
},
|
||||||
|
InsecureAllowUnencryptedAuth: j.C.Jabber.InsecureAllowUnencryptedAuth,
|
||||||
|
Debug: verboseClient,
|
||||||
|
Session: false,
|
||||||
|
Status: "xa",
|
||||||
|
StatusMessage: jabber.RandomPhrase(j.C.Jabber.StartupStatus),
|
||||||
|
DialTimeout: time.Duration(j.C.Jabber.ConnectionTimeout) * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Враппер основной программы. Фактически на каждой серьёзной ошибке, например, сетевой, mainLoop и некоторые
|
||||||
|
// вспомогательные корутины прекращает свою работу. Здесь мы это ловим и запускаем их снова. Задача в том, чтобы
|
||||||
|
// запустить все асинхронные процессы заново, с чистого листа, но уже с распарсенным конфигом.
|
||||||
|
|
||||||
|
// Устанавливаем соединение и гребём события, посылаемые сервером - основной и вспомогательные циклы программы.
|
||||||
|
j.GTomb.Go(func() error { return j.MyLoop() }) //nolint: gocritic
|
||||||
|
|
||||||
|
// Ловим первый же kill и не дождаемся остальных, хотя формально надо бы.
|
||||||
|
<-j.GTomb.Dying()
|
||||||
|
|
||||||
|
// Если у нас wire error, то вызов .Close() повлечёт за собой ошибку. А если у нас не wire error, то по ходу мы
|
||||||
|
// получим утечку сокетов.
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(j.C.Jabber.ReconnectDelay) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
1
cmd/buny-jabber-bot/version
Normal file
1
cmd/buny-jabber-bot/version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0.2
|
||||||
23
contrib/buny-jabber-bot.openrc
Normal file
23
contrib/buny-jabber-bot.openrc
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/sbin/openrc-run
|
||||||
|
|
||||||
|
name="$SVCNAME"
|
||||||
|
command="/var/lib/$SVCNAME/$SVCNAME"
|
||||||
|
supervisor="supervise-daemon"
|
||||||
|
pidfile="/run/$SVCNAME/$SVCNAME.pid"
|
||||||
|
command_user=nobody:nogroup
|
||||||
|
|
||||||
|
depend() {
|
||||||
|
need net redis
|
||||||
|
use dns
|
||||||
|
after redis
|
||||||
|
}
|
||||||
|
|
||||||
|
start_pre() {
|
||||||
|
mkdir -p /var/log/$SVCNAME
|
||||||
|
chown -R nobody:nogroup /var/log/$SVCNAME
|
||||||
|
mkdir -p /run/$SVCNAME
|
||||||
|
chown -R nobody:nogroup /run/$SVCNAME
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# vim: set ft=sh noet ai ts=4 sw=4 sts=4:
|
||||||
@@ -1,22 +1,38 @@
|
|||||||
{
|
{
|
||||||
"blacklist": [
|
"blacklist": [
|
||||||
{
|
{
|
||||||
# Пустой список означает глобальные баны, которые применяются ко всем комнатам, где присутствует бот
|
# Пустой список означает глобальные баны, которые применяются ко всем комнатам, где присутствует бот.
|
||||||
"room_name": "",
|
"room_name": "",
|
||||||
# Записывать ли в reason в банлисте комнаты сообщение вида autoban at yyyy.MM.dd hh:mm:ss
|
|
||||||
# Если не задано, то false
|
# Записывать ли в reason в банлисте комнаты сообщение вида autoban at yyyy.MM.dd hh:mm:ss.
|
||||||
|
# Если не задано, то false.
|
||||||
"reason_enable": false,
|
"reason_enable": false,
|
||||||
# Список регулярок JID-ов, которых надо банить
|
|
||||||
|
# Список регулярок JID-ов, которых надо банить.
|
||||||
"jid_re": [
|
"jid_re": [
|
||||||
"^[Mm]ary@server.tld/resource1$",
|
"^[Mm]ary@server.tld/resource1$",
|
||||||
"^[cC]hris(topher)?@",
|
"^[cC]hris(topher)?@",
|
||||||
"^(his|her)_cat@sweet.home/.*$",
|
"^(his|her)_cat@sweet.home/.*$",
|
||||||
"^(her|his)_dog@.*\\.street/.*"
|
"^(her|his)_dog@.*\\.street/.*"
|
||||||
],
|
],
|
||||||
# Список регулярок nick-ов злодеев
|
|
||||||
|
# Список регулярок nick-ов злодеев.
|
||||||
"nick_re": [
|
"nick_re": [
|
||||||
"^(Annoying_)?[F|f]ly$",
|
"^(Annoying_)?[F|f]ly$",
|
||||||
"^Buzzing_Wasp$"
|
"^Buzzing_Wasp$"
|
||||||
|
],
|
||||||
|
|
||||||
|
# Список регулярок, запрещённых к упоминанию в чяте.
|
||||||
|
"phrase_re": [
|
||||||
|
"^Exterminate.$"
|
||||||
|
],
|
||||||
|
|
||||||
|
"user_agent": [
|
||||||
|
{
|
||||||
|
"software": "BadUserAgent",
|
||||||
|
"version": "0.666.0",
|
||||||
|
"os": "DeathStarOS"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,48 +1,121 @@
|
|||||||
|
/* vim: set ft=text noet ai ts=2 sw=2 sts=2: */
|
||||||
{
|
{
|
||||||
# Фронт-энд бота
|
# Фронт-энд бота
|
||||||
"jabber": {
|
"jabber": {
|
||||||
# Если не задан, будет localhost
|
# Если не задан, будет localhost
|
||||||
"server": "jabber.tld",
|
"server": "jabber.tld",
|
||||||
|
|
||||||
# Если не задано - 5222 для нешифрованного соединения или для starttls и 5223 для голого (старорежимного) ssl
|
# Если не задано - 5222 для нешифрованного соединения или для starttls и 5223 для голого (старорежимного) ssl
|
||||||
"port": 5222,
|
"port": 5222,
|
||||||
|
|
||||||
# Включить шифрование
|
# Включить шифрование
|
||||||
"ssl": true,
|
"ssl": true,
|
||||||
|
|
||||||
# Это более современный протокол и как раз на нём работет большинство jabber-серверов, порт 5222,
|
# Это более современный протокол и как раз на нём работет большинство jabber-серверов, порт 5222,
|
||||||
# jabber.ru по этому протоколу как-то странно работает, если его включить, я получаю
|
# jabber.ru по этому протоколу как-то странно работает, если его включить, я получаю
|
||||||
# tls: first record does not look like a TLS handshake
|
# tls: first record does not look like a TLS handshake
|
||||||
"starttls" : true,
|
"starttls" : true,
|
||||||
|
|
||||||
# Не проверяем сертификаты по-умолчанию.
|
# Не проверяем сертификаты по-умолчанию.
|
||||||
# Вам правда хочется всё это проверять? Там по идее могут быть всякие Lets Encrypt, которые хоть и работают, но
|
# Вам правда хочется всё это проверять? Там по идее могут быть всякие Lets Encrypt, которые хоть и работают, но
|
||||||
# создают кучу проблем
|
# создают кучу проблем
|
||||||
"ssl_verify": false,
|
"ssl_verify": false,
|
||||||
|
|
||||||
# Позволять авторизацию логин-паролем по незашифрованному каналу связи
|
# Позволять авторизацию логин-паролем по незашифрованному каналу связи
|
||||||
"insecureallowunencryptedauth": false,
|
"insecureallowunencryptedauth": false,
|
||||||
|
|
||||||
# Таймаут установления соединения с сервером, в секундах. 10 секунд, если не задано.
|
# Таймаут установления соединения с сервером, в секундах. 10 секунд, если не задано.
|
||||||
"connection_timeout": 10,
|
"connection_timeout": 10,
|
||||||
|
|
||||||
# Задержка перед пере-коннекчиванием. 3 секунды, если не задано.
|
# Задержка перед пере-коннекчиванием. 3 секунды, если не задано.
|
||||||
"reconnect_delay": 3,
|
"reconnect_delay": 3,
|
||||||
|
|
||||||
# Промежуток между пингами сервера, для определения его живости. По-умолчанию 60 секунд.
|
# Промежуток между пингами сервера, для определения его живости. По-умолчанию 60 секунд.
|
||||||
# Пинги посылаются, если в течение этого времени не было активности со стороны сервера.
|
# Пинги посылаются, если в течение этого времени не было активности со стороны сервера.
|
||||||
"server_ping_delay": 60,
|
"server_ping_delay": 60,
|
||||||
|
|
||||||
# Промежуток между пингами muc-ов. По-умолчанию 900 секунд. (Согласно рекомендации в xep-0410.)
|
# Промежуток между пингами muc-ов. По-умолчанию 900 секунд. (Согласно рекомендации в xep-0410.)
|
||||||
# Пинги посылаются, если в течение этого времени не было активности со стороны muc-а.
|
# Пинги посылаются, если в течение этого времени не было активности со стороны muc-а.
|
||||||
"muc_ping_delay": 900,
|
"muc_ping_delay": 900,
|
||||||
|
|
||||||
# Сколько ждать перед переджоиванием к комнате. По-умолчанию, 3 секунды.
|
# Сколько ждать перед переджоиванием к комнате. По-умолчанию, 3 секунды.
|
||||||
"muc_rejoin_delay": 3,
|
"muc_rejoin_delay": 3,
|
||||||
|
|
||||||
# Ping-to-ping splay delay. Варьируемая задержка между пингами, чтобы не набирать высокий message rate.
|
# Ping-to-ping splay delay. Варьируемая задержка между пингами, чтобы не набирать высокий message rate.
|
||||||
# По-умолчанию, 3 секунды. Под капотом преобразуются в миллисекунды. Важна для пингов MUC-ов, особенно,
|
# По-умолчанию, 3 секунды. Под капотом преобразуются в миллисекунды. Важна для пингов MUC-ов, особенно,
|
||||||
# если их МНОГО.
|
# если их МНОГО.
|
||||||
"ping_splay_delay": 3,
|
"ping_splay_delay": 3,
|
||||||
|
|
||||||
# Ник бота, с которым он будет заходить в комнаты
|
# Ник бота, с которым он будет заходить в комнаты
|
||||||
"nick": "buny",
|
"nick": "buny",
|
||||||
|
|
||||||
# Уникальная часть jid-а, специфичная для клиента
|
# Уникальная часть jid-а, специфичная для клиента
|
||||||
"resource" : "Лунная призма",
|
"resource" : "Лунная призма",
|
||||||
|
|
||||||
|
# Имя пользователя для авторизации на сервере
|
||||||
|
"user": "buny@jabber.tld",
|
||||||
|
|
||||||
|
# Пароль для авторизации
|
||||||
|
"password": "secret",
|
||||||
|
|
||||||
|
# Список JID-ов, которым позволено управлять ботом (например, давать отмашку на rehash).
|
||||||
|
# В списке jid-ы без resource-а.
|
||||||
|
# Если не задано ни одного мастера, то бот сам себе мастер.
|
||||||
|
"bot_masters": [
|
||||||
|
"luna@earth.tld"
|
||||||
|
],
|
||||||
|
|
||||||
|
# Jabber-каналы, к которым бот попробует присоединиться при старте
|
||||||
|
"channels": [
|
||||||
|
{
|
||||||
|
# Название канала, обязательное поле
|
||||||
|
"name" : "my_channel@conference.jabber.tld",
|
||||||
|
|
||||||
|
# Под каким ником бот зайдёт конкретно на этот канал. Если не задано, будет использован ник, заданный в
|
||||||
|
# общих настройках Jabber, выше по конфигу.
|
||||||
|
"nick" : "sailormoon",
|
||||||
|
|
||||||
|
# Настройки байесовского классификатора. В виду вероятности ложного срабатывания возможно, имеет смысл
|
||||||
|
# воздержаться от использования. Зависит от использования и от словарей.
|
||||||
|
"bayes": {
|
||||||
|
# Если не указано, то он выключен
|
||||||
|
"enabled": false,
|
||||||
|
|
||||||
|
# Минимальное количество слов во фразе, по-умолчанию 8, если не указано или указано меньше
|
||||||
|
"min_words": 10,
|
||||||
|
|
||||||
|
# Минимальная длина фразы, по-умолчанию 40 символов, если не указано или указано меньше
|
||||||
|
"min_length": 50,
|
||||||
|
|
||||||
|
# Действие по умолчанию log, devoice, kick, ban. Если не задано, то log.
|
||||||
|
"default_action": "kick"
|
||||||
|
},
|
||||||
|
"password": ""
|
||||||
|
|
||||||
|
# Проверка на фразы, написанные капсом
|
||||||
|
"all_caps": {
|
||||||
|
# Если не указано, то выключено
|
||||||
|
"enabled": false,
|
||||||
|
|
||||||
|
# Минимальное количество буквенных символов в проверяемой фразе
|
||||||
|
"min_length": 15,
|
||||||
|
|
||||||
|
# Действие по-умолчанию log, devoice, kick, ban. Если не задано, то log.
|
||||||
|
"default_action": "kick"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name" : "another_channel@conference.jabber.tld"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
# Статус, с которым бот запускается. (если всё нормально, этот статус будет видать только в логах сервера и
|
# Статус, с которым бот запускается. (если всё нормально, этот статус будет видать только в логах сервера и
|
||||||
# на долю секунды он промелькнёт у всех в ростере, у кого бот в знакомых. Может отсуствовать, тогда статус будет
|
# на долю секунды он промелькнёт у всех в ростере, у кого бот в знакомых. Может отсуствовать, тогда статус будет
|
||||||
# пустой строкой
|
# пустой строкой
|
||||||
"startup_status": [
|
"startup_status": [
|
||||||
"bunny bot is starting"
|
"bunny bot is starting"
|
||||||
],
|
],
|
||||||
|
|
||||||
# Статус, который демонстрируется во время работы бота, как xa
|
# Статус, который демонстрируется во время работы бота, как xa
|
||||||
# (по задумке - extended away, на практике, к away обычно отношения не имеет)
|
# (по задумке - extended away, на практике, к away обычно отношения не имеет)
|
||||||
"runtime_status": {
|
"runtime_status": {
|
||||||
@@ -62,15 +135,23 @@
|
|||||||
"Звезда нашей надежды никогда не исчезнет. Пока она горит, всё будет в порядке. Мы не сможем проиграть",
|
"Звезда нашей надежды никогда не исчезнет. Пока она горит, всё будет в порядке. Мы не сможем проиграть",
|
||||||
"Голова с хвостиками… Не вмешивайся в наши дела! Играть в войну — это опасно"
|
"Голова с хвостиками… Не вмешивайся в наши дела! Играть в войну — это опасно"
|
||||||
],
|
],
|
||||||
|
|
||||||
# Период смены статуса, сек. Если параметра нет или он выставлен в 0, ротации не будет
|
# Период смены статуса, сек. Если параметра нет или он выставлен в 0, ротации не будет
|
||||||
# 3 дня
|
# 3 дня
|
||||||
"rotation_time": 259200,
|
"rotation_time": 259200,
|
||||||
|
|
||||||
# Рандомная составляющая, добавляемая к периоду смены статуса, сек.
|
# Рандомная составляющая, добавляемая к периоду смены статуса, сек.
|
||||||
# 4 дня
|
# 4 дня
|
||||||
"rotation_splay_time": 345600
|
"rotation_splay_time": 345600
|
||||||
},
|
},
|
||||||
|
|
||||||
|
# Задержка перед баном, миллисекунды. Иногда сервер может подтормаживать на моменте бана. Jabber.ru так делает.
|
||||||
|
# В этом случае бан не записывается в банлист комнаты.
|
||||||
|
"ban_delay": 600,
|
||||||
|
|
||||||
# Произносим ли что-то пафосное в момоент бана
|
# Произносим ли что-то пафосное в момоент бана
|
||||||
"ban_phrases_enable": false,
|
"ban_phrases_enable": false,
|
||||||
|
|
||||||
# Пафосные фраза, произносимые перед баном
|
# Пафосные фраза, произносимые перед баном
|
||||||
"ban_phrases": [
|
"ban_phrases": [
|
||||||
"Во имя Луны, я накажу тебя!",
|
"Во имя Луны, я накажу тебя!",
|
||||||
@@ -103,20 +184,15 @@
|
|||||||
"Мы сражаемся за мир и равенство!",
|
"Мы сражаемся за мир и равенство!",
|
||||||
"Мы — надежда для всех, кто нуждается в защите!",
|
"Мы — надежда для всех, кто нуждается в защите!",
|
||||||
"Мы — воительницы справедливости, и ничто не остановит нас!"
|
"Мы — воительницы справедливости, и ничто не остановит нас!"
|
||||||
],
|
|
||||||
# Имя пользователя для авторизации на сервере
|
|
||||||
"user": "buny@jabber.tld",
|
|
||||||
# Пароль для авторизации
|
|
||||||
"password": "secret",
|
|
||||||
# Jabber-каналы, к которым бот попробует присоединиться при старте
|
|
||||||
"channels": [
|
|
||||||
"my_channel@conference.jabber.tld",
|
|
||||||
"another_channel@conference.jabber.tld"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
# Многословность логов. Если не задано, то info. Debug - ДЕЙСТВИТЕЛЬНО вербозный уровень логгирования.
|
# command sign символ-суффикс команды, например !rehash или !help. По-умолчанию "!".
|
||||||
# Есть ещё trace :)
|
"csign" : "!",
|
||||||
|
|
||||||
|
# Многословность логов. Если не задано, то info.
|
||||||
|
# Есть разные уровни trace, debug, debug, info, warn, error, fatal.
|
||||||
|
# Практически задействованы только trace, debug, info.
|
||||||
"loglevel": "debug",
|
"loglevel": "debug",
|
||||||
|
|
||||||
# Если не задан, логи идут в STDOUT
|
# Если не задан, логи идут в STDOUT
|
||||||
|
|||||||
69
globals.go
69
globals.go
@@ -1,69 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/eleksir/go-xmpp"
|
|
||||||
"gopkg.in/tomb.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config - это у нас глобальная штука :) .
|
|
||||||
var config myConfig
|
|
||||||
|
|
||||||
// Списки тех, кого мы не баним никогда.
|
|
||||||
var whiteList myWhiteList
|
|
||||||
|
|
||||||
// Списки злодеев, которых мы баним.
|
|
||||||
var blackList myBlackList
|
|
||||||
|
|
||||||
// Ставится в true, если мы получили сигнал на выключение.
|
|
||||||
var shutdown = false
|
|
||||||
|
|
||||||
// Чтобы не организовывать драку за установку коннекта.
|
|
||||||
var connecting = false
|
|
||||||
|
|
||||||
// Глобальное состояние соединения.
|
|
||||||
var isConnected = false
|
|
||||||
|
|
||||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
|
||||||
var sigChan = make(chan os.Signal, 1)
|
|
||||||
|
|
||||||
// Основной инстанс xmpp-клиента.
|
|
||||||
var talk *xmpp.Client
|
|
||||||
|
|
||||||
// Опции подключения к xmpp-серверу.
|
|
||||||
var options *xmpp.Options
|
|
||||||
|
|
||||||
// Список комнат, в которых находится бот.
|
|
||||||
var roomsConnected []string
|
|
||||||
|
|
||||||
// Время последней активности, нужно для jabber:iq:last.
|
|
||||||
var lastActivity int64
|
|
||||||
|
|
||||||
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
|
|
||||||
// сервера.
|
|
||||||
var lastServerActivity int64
|
|
||||||
|
|
||||||
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
|
||||||
// muc-ов.
|
|
||||||
var lastMucActivity *Collection
|
|
||||||
|
|
||||||
// Получен ли ответ на запрос disco#info к серверу.
|
|
||||||
var serverCapsQueried bool
|
|
||||||
|
|
||||||
// sync.Map-ка с капабилити сервера.
|
|
||||||
var serverCapsList *Collection
|
|
||||||
|
|
||||||
// sync.Map-ка с комнатами и их capability.
|
|
||||||
var mucCapsList *Collection
|
|
||||||
|
|
||||||
// Время, когда был отправлен c2s ping.
|
|
||||||
var serverPingTimestampTx int64
|
|
||||||
|
|
||||||
// Время, когда был принят s2c pong.
|
|
||||||
var serverPingTimestampRx int64
|
|
||||||
|
|
||||||
// Объектик для хранения стейта утилизатора горутинок.
|
|
||||||
var gTomb tomb.Tomb
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
16
go.mod
16
go.mod
@@ -1,17 +1,21 @@
|
|||||||
module buny-jabber-bot
|
module buny-jabber-bot
|
||||||
|
|
||||||
go 1.20
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/eleksir/go-xmpp v1.5.0
|
github.com/eleksir/go-xmpp v1.6.0
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible
|
github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
|
github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/crypto v0.12.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/net v0.40.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
28
go.sum
28
go.sum
@@ -1,10 +1,14 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/eleksir/go-xmpp v1.5.0 h1:kAjpCxuqWiu9LdlCTkQzvczotHONpojwp/mpI4aBVmQ=
|
github.com/eleksir/go-xmpp v1.6.0 h1:bkGr0YVczJWpkyk/IHhZb2ewv+NeHLvdNfds0TFgqao=
|
||||||
github.com/eleksir/go-xmpp v1.5.0/go.mod h1:KnvbXvUpttEVFl8l5eDO2Q7J97ZxtTMvOsDWULD2Rn8=
|
github.com/eleksir/go-xmpp v1.6.0/go.mod h1:Ii6Wemk+yuGQOF+Glt0syjvtcPvdaBLstuNp/+rRJrg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
|
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
||||||
|
github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a h1:zSc8tkEGo49/E6cQ9o00si0L9Q/Lu1Hsg8JWKCvzEDU=
|
||||||
|
github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a/go.mod h1:SELxwZQq/mPnfPCR2mchLmT4TQaPJvYtLcCtDWSM7vM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
@@ -12,15 +16,19 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
|
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
102
internal/jabber/bf.go
Normal file
102
internal/jabber/bf.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jbrukh/bayesian"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
goodDictionaryFile string = "./data/good_dictionary.txt"
|
||||||
|
badDictionaryFile string = "./data/bad_dictionary.txt"
|
||||||
|
dataFile string = "./data/data.bin"
|
||||||
|
Bad bayesian.Class = "Bad"
|
||||||
|
Good bayesian.Class = "Good"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Выдаёт "очки похожести" для данной фразы.
|
||||||
|
func checkPhrase(s string) error { //nolint: unused
|
||||||
|
classifier, err := bayesian.NewClassifierFromFile(dataFile)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open %s: %w", dataFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s = nStringLower(s)
|
||||||
|
|
||||||
|
scores, _, _ := classifier.LogScores(strings.Split(s, " "))
|
||||||
|
|
||||||
|
_, err = fmt.Printf("Score %v\n", scores[0])
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выучивает слова из предопределённых словарей.
|
||||||
|
func learn() error { //nolint: unused
|
||||||
|
classifier := bayesian.NewClassifier(Bad, Good)
|
||||||
|
|
||||||
|
if err := feed(classifier, goodDictionaryFile, Good); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := feed(classifier, badDictionaryFile, Bad); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := classifier.WriteToFile(dataFile); err != nil {
|
||||||
|
return fmt.Errorf("unable to save data to file %s: %w", dataFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дополняет заданный класс фразами из указанного файла.
|
||||||
|
func feed(c *bayesian.Classifier, filename string, bc bayesian.Class) error { //nolint:unused
|
||||||
|
fh, err := os.Open(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open file %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(fh *os.File) {
|
||||||
|
if err := fh.Close(); err != nil {
|
||||||
|
log.Errorf("unable to close %s cleanly: %s", filename, err)
|
||||||
|
}
|
||||||
|
}(fh)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(fh)
|
||||||
|
|
||||||
|
for {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
|
||||||
|
line = nStringLower(line)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Правильно обрабатываем последнюю строку и конец файла.
|
||||||
|
if err == io.EOF {
|
||||||
|
if line != "" {
|
||||||
|
stuff := strings.Split(line, " ")
|
||||||
|
c.Learn(stuff, bc)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to read %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if line != "" {
|
||||||
|
stuff := strings.Split(line, " ")
|
||||||
|
c.Learn(stuff, bc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
206
internal/jabber/black-whilte_lists.go
Normal file
206
internal/jabber/black-whilte_lists.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hjson/hjson-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadWhitelist читает и валидирует белые списки пользователей.
|
||||||
|
func (j *Jabber) ReadWhitelist() error {
|
||||||
|
var (
|
||||||
|
whitelistLoaded = false
|
||||||
|
err error
|
||||||
|
executablePath string
|
||||||
|
)
|
||||||
|
|
||||||
|
executablePath, err = os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("unable to get current executable path: %w", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistJSONPath := fmt.Sprintf("%s/data/whitelist.json", filepath.Dir(executablePath))
|
||||||
|
|
||||||
|
locations := []string{
|
||||||
|
"~/.buny-jabber-bot-whitelist.json",
|
||||||
|
"~/buny-jabber-bot-whitelist.json",
|
||||||
|
"/etc/buny-jabber-bot-whitelist.json",
|
||||||
|
whitelistJSONPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, location := range locations {
|
||||||
|
fileInfo, err := os.Stat(location)
|
||||||
|
|
||||||
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Файл белого списка длинноват для белого списка, попробуем следующего кандидата
|
||||||
|
if fileInfo.Size() > 2097152 {
|
||||||
|
log.Warnf("Whitelist file %s is too long for whitelist, skipping", location)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(location)
|
||||||
|
|
||||||
|
// Не удалось прочитать, попробуем следующего кандидата
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip reading whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
|
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||||
|
var (
|
||||||
|
sampleWhitelist MyWhiteList
|
||||||
|
tmp map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
|
// Не удалось распарсить - попробуем следующего кандидата
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpJSON, err := json.Marshal(tmp)
|
||||||
|
|
||||||
|
// Не удалось преобразовать map-ку в json
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(tmpJSON, &sampleWhitelist); err != nil {
|
||||||
|
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
j.WhiteList = sampleWhitelist
|
||||||
|
whitelistLoaded = true
|
||||||
|
|
||||||
|
log.Infof("Using %s as whiteList file", location)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !whitelistLoaded {
|
||||||
|
return errors.New("whitelist was not loaded") //nolint:goerr113
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBlacklist читает и валидирует чёрные списки пользователей.
|
||||||
|
func (j *Jabber) ReadBlacklist() error {
|
||||||
|
var (
|
||||||
|
blacklistLoaded = false
|
||||||
|
err error
|
||||||
|
executablePath string
|
||||||
|
)
|
||||||
|
|
||||||
|
executablePath, err = os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("unable to get current executable path: %w", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
whitelistJSONPath := fmt.Sprintf("%s/data/blacklist.json", filepath.Dir(executablePath))
|
||||||
|
|
||||||
|
locations := []string{
|
||||||
|
"~/.bunyPresense-jabber-bot-blacklist.json",
|
||||||
|
"~/bunyPresense-jabber-bot-blacklist.json",
|
||||||
|
"/etc/bunyPresense-jabber-bot-blacklist.json",
|
||||||
|
whitelistJSONPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, location := range locations {
|
||||||
|
fileInfo, err := os.Stat(location)
|
||||||
|
|
||||||
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Файл чёрного списка длинноват для чёрного списка, попробуем следующего кандидата
|
||||||
|
if fileInfo.Size() > 16777216 {
|
||||||
|
log.Warnf("Blacklist file %s is too long for blacklist, skipping", location)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(location)
|
||||||
|
|
||||||
|
// Не удалось прочитать, попробуем следующего кандидата
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip reading blacklist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
|
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||||
|
var (
|
||||||
|
sampleBlacklist MyBlackList
|
||||||
|
tmp map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
|
// Не удалось распарсить - попробуем следующего кандидата
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing blacklist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpJSON, err := json.Marshal(tmp)
|
||||||
|
|
||||||
|
// Не удалось преобразовать map-ку в json
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing blacklist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(tmpJSON, &sampleBlacklist); err != nil {
|
||||||
|
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
j.BlackList = sampleBlacklist
|
||||||
|
blacklistLoaded = true
|
||||||
|
|
||||||
|
log.Infof("Using %s as blacklist file", location)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !blacklistLoaded {
|
||||||
|
return errors.New("blacklist was not loaded") //nolint:goerr113
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
603
internal/jabber/buny.go
Normal file
603
internal/jabber/buny.go
Normal file
@@ -0,0 +1,603 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то
|
||||||
|
// отправляет его в бан.
|
||||||
|
func (j *Jabber) BunyPresense(v xmpp.Presence) error { //nolint:gocognit,gocyclo
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Если у presence-а есть JID и presence из одной из комнат, в которой мы есть и если его домен в чёрном
|
||||||
|
// списке, заносим пидора в список outcast-ов
|
||||||
|
if v.JID != "" {
|
||||||
|
// На всякий случай: себя никогда не баним, явным образом
|
||||||
|
if v.JID == j.Talk.JID() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
|
||||||
|
if room == "" {
|
||||||
|
log.Infof("We got empty room field in presence event, which kinda strange: %s", spew.Sdump(v))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
goodJid := strings.SplitN(v.JID, "/", 2)[0]
|
||||||
|
|
||||||
|
// Обрабатываем правила белого списка
|
||||||
|
for _, good := range j.WhiteList.Whitelist {
|
||||||
|
// Глобальный белый список
|
||||||
|
if good.RoomName == "" {
|
||||||
|
for _, person := range good.Jid {
|
||||||
|
if person == goodJid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Белый список для конкретных каналов
|
||||||
|
if good.RoomName == room {
|
||||||
|
for _, person := range good.Jid {
|
||||||
|
if person == goodJid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := j.QuerySoftwareVersion(v.From)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to query user software and version: jid=%s id=%s, err=%w",
|
||||||
|
v.JID,
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
evilJid := strings.SplitN(v.JID, "/", 2)[0]
|
||||||
|
evilNick := ""
|
||||||
|
evilNicks := strings.SplitN(v.From, "/", 2)
|
||||||
|
|
||||||
|
// Давайте-ка похэндлим нонсенс - когда у нас в строке нету разделителя
|
||||||
|
if len(evilNicks) > 1 {
|
||||||
|
evilNick = evilNicks[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем правила чёрного списка
|
||||||
|
for _, cRoom := range j.RoomsConnected {
|
||||||
|
if cRoom == room {
|
||||||
|
for _, bEntry := range j.BlackList.Blacklist {
|
||||||
|
// Обработаем правила глобального чёрного списка
|
||||||
|
if bEntry.RoomName == "" {
|
||||||
|
for _, jidRegexp := range bEntry.JidRe {
|
||||||
|
if jidRegexp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(jidRegexp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Incorrect regexp in global blacklist: %s, skipping", jidRegexp)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking jid %s vs global blacklist regex %s", v.JID, jidRegexp)
|
||||||
|
|
||||||
|
if re.MatchString(v.JID) {
|
||||||
|
log.Warnf(
|
||||||
|
"Hammer falls on %s (%s): jid matches with global blacklist entry: %s",
|
||||||
|
v.From,
|
||||||
|
evilJid,
|
||||||
|
jidRegexp,
|
||||||
|
)
|
||||||
|
|
||||||
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, nickRegexp := range bEntry.NickRe {
|
||||||
|
if nickRegexp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(nickRegexp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Incorrect regexp in global blacklist: %s, skipping", nickRegexp)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking nick %s vs global blacklist regex %s", evilNick, nickRegexp)
|
||||||
|
|
||||||
|
if re.MatchString(evilNick) {
|
||||||
|
log.Warnf(
|
||||||
|
"Hammer falls on %s (%s): nick matches with global blacklist entry: %s",
|
||||||
|
v.From,
|
||||||
|
evilJid,
|
||||||
|
nickRegexp,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Баним именно jid
|
||||||
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработаем правила конкретного канала, комнаты конференции
|
||||||
|
if bEntry.RoomName == room {
|
||||||
|
for _, jidRegexp := range bEntry.JidRe {
|
||||||
|
if jidRegexp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(jidRegexp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, jidRegexp)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking jid %s vs room %s blacklist regex %s", v.JID, room, jidRegexp)
|
||||||
|
|
||||||
|
if re.MatchString(v.JID) {
|
||||||
|
log.Warnf(
|
||||||
|
"Hammer falls on %s (%s): jid matches with room blacklist entry: %s",
|
||||||
|
v.From,
|
||||||
|
evilJid,
|
||||||
|
jidRegexp,
|
||||||
|
)
|
||||||
|
|
||||||
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bEntry.RoomName == room {
|
||||||
|
for _, nickRegexp := range bEntry.NickRe {
|
||||||
|
if nickRegexp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(nickRegexp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, nickRegexp)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking nick %s vs room %s blacklist regex %s", evilJid, room, nickRegexp)
|
||||||
|
|
||||||
|
if re.MatchString(evilNick) {
|
||||||
|
log.Warnf(
|
||||||
|
"Hammer falls on %s (%s): nick matches with room blacklist entry: %s",
|
||||||
|
v.From,
|
||||||
|
evilJid,
|
||||||
|
nickRegexp,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Баним именно jid
|
||||||
|
id, err := j.Squash(room, evilJid, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BunyChat производит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого
|
||||||
|
// шаблона банит участника чата.
|
||||||
|
func (j *Jabber) BunyChat(v xmpp.Chat) error {
|
||||||
|
var (
|
||||||
|
room = (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
// nick = (strings.SplitN(v.Remote, "/", 2))[1]
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Действовать мы можем только в рамках тех комнат, где явно присуствуем.
|
||||||
|
for _, cRoom := range j.RoomsConnected {
|
||||||
|
if cRoom == room {
|
||||||
|
// Перебирём правила чёрных списков.
|
||||||
|
for _, bEntry := range j.BlackList.Blacklist {
|
||||||
|
// Обработаем правила глобального чёрного списка
|
||||||
|
if bEntry.RoomName == "" {
|
||||||
|
for _, phraseRegexp := range bEntry.PhraseRe {
|
||||||
|
if phraseRegexp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(phraseRegexp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, phraseRegexp)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp)
|
||||||
|
|
||||||
|
if re.MatchString(v.Text) {
|
||||||
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
|
log.Warnf(
|
||||||
|
"Hammer falls on %s (%s): phrase matches with global blacklist entry: %s vs %s",
|
||||||
|
v.Remote,
|
||||||
|
realJID,
|
||||||
|
v.Text,
|
||||||
|
phraseRegexp,
|
||||||
|
)
|
||||||
|
|
||||||
|
if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработаем правила чёрного списка для конкретной комнаты
|
||||||
|
if bEntry.RoomName == room {
|
||||||
|
for _, phraseRegexp := range bEntry.PhraseRe {
|
||||||
|
if phraseRegexp == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
re, err := regexp.Compile(phraseRegexp)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Incorrect regexp in room %s blacklist: %s, skipping", room, phraseRegexp)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Checking phrase %s vs room %s blacklist regex %s", v.Text, room, phraseRegexp)
|
||||||
|
|
||||||
|
if re.MatchString(v.Text) {
|
||||||
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
|
log.Warnf(
|
||||||
|
"Hammer falls on %s (%s): phrase matches with room blacklist entry: %s vs %s",
|
||||||
|
v.Remote,
|
||||||
|
realJID,
|
||||||
|
v.Text,
|
||||||
|
phraseRegexp,
|
||||||
|
)
|
||||||
|
|
||||||
|
if id, err := j.Squash(room, realJID, bEntry.ReasonEnable, v.Type); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если включено, проверяем фразу на КАПС.
|
||||||
|
for _, channel := range j.C.Jabber.Channels {
|
||||||
|
if channel.Name == room && channel.AllCaps.Enabled {
|
||||||
|
// Нормализуем строку и вырежем из неё пробелы
|
||||||
|
normPhrase := strings.ReplaceAll(nString(v.Text), " ", "")
|
||||||
|
|
||||||
|
// Проверяем согласно тому, что длина фразы более чем сколько-то символов
|
||||||
|
if len(normPhrase) >= channel.AllCaps.MinLength {
|
||||||
|
normPhraseUpper := strings.ReplaceAll(nStringUpper(v.Text), " ", "")
|
||||||
|
|
||||||
|
if normPhrase == normPhraseUpper {
|
||||||
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
id, err := j.Squash(room, realJID, false, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BunySoftwareVersion производит проверку по чёрному списку версий и названий клинтского ПО.
|
||||||
|
func (j *Jabber) BunySoftwareVersion(v xmpp.IQ, ver IqResultSoftwareVersion) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if v.From != "" {
|
||||||
|
// На всякий случай: себя никогда не баним, явным образом
|
||||||
|
if v.From == j.Talk.JID() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
|
||||||
|
var (
|
||||||
|
presenceJSONStrings []string
|
||||||
|
)
|
||||||
|
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSONStrings = InterfaceToStringSlice(presenceJSONInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Looking up %s in presence db", v.From)
|
||||||
|
|
||||||
|
for _, presenceJSONString := range presenceJSONStrings {
|
||||||
|
var p xmpp.Presence
|
||||||
|
_ = json.Unmarshal([]byte(presenceJSONString), &p)
|
||||||
|
|
||||||
|
if p.Affiliation == "admin" || p.Affiliation == "owner" {
|
||||||
|
log.Infof("Skipping useragent blacklist check for %s because his affiliation is %s", v.From, p.Affiliation)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// На всякий случай.
|
||||||
|
if p.Role == "moderator" {
|
||||||
|
log.Infof("Skipping useragent blacklist check for %s because his role is %s", v.From, p.Role)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.From == p.From {
|
||||||
|
log.Errorf("Found jid of %s in presence db: %s", v.From, p.JID)
|
||||||
|
|
||||||
|
// Lookup in global blacklist.
|
||||||
|
for _, cRoom := range j.RoomsConnected {
|
||||||
|
if cRoom == room {
|
||||||
|
for _, bEntry := range j.BlackList.Blacklist {
|
||||||
|
// Обработаем правила глобального чёрного списка
|
||||||
|
if bEntry.RoomName == "" {
|
||||||
|
for _, useragent := range bEntry.UserAgent {
|
||||||
|
switch {
|
||||||
|
case useragent.Name != "" && useragent.Version != "" && useragent.Os != "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case useragent.Name != "" && useragent.Version != "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case useragent.Name != "":
|
||||||
|
if ver.Name == useragent.Name {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case useragent.Version != "":
|
||||||
|
if ver.Version == useragent.Version {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bEntry.RoomName == room {
|
||||||
|
for _, useragent := range bEntry.UserAgent {
|
||||||
|
switch {
|
||||||
|
case useragent.Name != "" && useragent.Version != "" && useragent.Os != "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case useragent.Name != "" && useragent.Version != "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case useragent.Name != "":
|
||||||
|
if ver.Name == useragent.Name {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case useragent.Version != "":
|
||||||
|
if ver.Version == useragent.Version {
|
||||||
|
id, err := j.Squash(room, p.JID, bEntry.ReasonEnable, v.Type)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package jabber
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
250
internal/jabber/commands.go
Normal file
250
internal/jabber/commands.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cmd парсит команды из чятика.
|
||||||
|
func (j *Jabber) Cmd(v xmpp.Chat) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case v.Text == fmt.Sprintf("%shelp", j.C.CSign) || v.Text == fmt.Sprintf("%sпомощь", j.C.CSign):
|
||||||
|
var (
|
||||||
|
answer string
|
||||||
|
chosenOneTalks = false
|
||||||
|
)
|
||||||
|
|
||||||
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
|
for _, master := range j.C.Jabber.BotMasters {
|
||||||
|
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
||||||
|
chosenOneTalks = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosenOneTalks {
|
||||||
|
answer = fmt.Sprintf("%sпомощь - этот список команд\n", j.C.CSign)
|
||||||
|
answer += fmt.Sprintf("%shelp - this commands list\n", j.C.CSign)
|
||||||
|
answer += fmt.Sprintf("%srehash - reload white and black lists (available to bot admins only)\n", j.C.CSign)
|
||||||
|
answer += fmt.Sprintf("%sver|%sversion - prints version of software", j.C.CSign, j.C.CSign)
|
||||||
|
} else {
|
||||||
|
answer = "Ничем помочь не могу. Луна не светит на тебя."
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := v.Remote
|
||||||
|
|
||||||
|
if v.Type == "groupchat" {
|
||||||
|
dest = (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(
|
||||||
|
xmpp.Chat{ //nolint:exhaustruct
|
||||||
|
Remote: dest,
|
||||||
|
Text: strings.TrimSpace(answer),
|
||||||
|
Type: v.Type,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
err = fmt.Errorf("unable to send message to %s: %w", v.Remote, err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case v.Text == fmt.Sprintf("%srehash", j.C.CSign): //nolint:wsl
|
||||||
|
/* Эта команда может прилетать из приватной беседы с realjid-ом, из приватной беседы с chat-nick-ом, а также из
|
||||||
|
* чятика.
|
||||||
|
* Groupchat мы можем определить по v.Type (groupchat) и тогда понятно что делать.
|
||||||
|
* А вот, например, приватные притязания определить несколько сложно-вато. Но можно попробовать, например,
|
||||||
|
* пытаться по списку presence-ов находить персонажа и его real jid и уже из этого определять master он или нет.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Попробуем определить каков алгоритм наших действий, в зависимости от типа сообщения
|
||||||
|
switch v.Type {
|
||||||
|
case "groupchat":
|
||||||
|
// Проверим, является ли "командир" избранным или это самозванец?
|
||||||
|
var (
|
||||||
|
chosenOneTalks = false
|
||||||
|
listsLoaded = true
|
||||||
|
)
|
||||||
|
|
||||||
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
|
for _, master := range j.C.Jabber.BotMasters {
|
||||||
|
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
||||||
|
chosenOneTalks = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !chosenOneTalks {
|
||||||
|
log.Infof(
|
||||||
|
"Command %srehash given by non-bot_master user %s(%s), ignoring",
|
||||||
|
j.C.CSign,
|
||||||
|
realJID,
|
||||||
|
v.Remote,
|
||||||
|
)
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = "Ничем помочь не могу. Луна не светит на тебя."
|
||||||
|
|
||||||
|
_, err := j.Talk.Send(msg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.ReadWhitelist(); err != nil {
|
||||||
|
listsLoaded = false
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = "chat" // private message
|
||||||
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.ReadBlacklist(); err != nil {
|
||||||
|
listsLoaded = false
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = "chat" // private message
|
||||||
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if listsLoaded {
|
||||||
|
room := (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = room
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = "Сделано"
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "chat":
|
||||||
|
room := (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
|
||||||
|
// Реагировать на приватную команду "из комнаты", только если бот в комнате
|
||||||
|
if slices.Contains(j.RoomsConnected, room) {
|
||||||
|
var (
|
||||||
|
chosenOneTalks = false
|
||||||
|
listsLoaded = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Реагировать на rehash только если собеседник является bot master-ом
|
||||||
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
|
for _, master := range j.C.Jabber.BotMasters {
|
||||||
|
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
||||||
|
chosenOneTalks = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !chosenOneTalks {
|
||||||
|
log.Infof(
|
||||||
|
"Command %srehash given by non-bot_master user %s(%s), ignoring",
|
||||||
|
j.C.CSign,
|
||||||
|
realJID,
|
||||||
|
v.Remote,
|
||||||
|
)
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = "Ничем помочь не могу. Луна не светит на тебя."
|
||||||
|
|
||||||
|
_, err := j.Talk.Send(msg)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.ReadWhitelist(); err != nil {
|
||||||
|
listsLoaded = false
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.ReadBlacklist(); err != nil {
|
||||||
|
listsLoaded = false
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if listsLoaded {
|
||||||
|
var msg xmpp.Chat
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = "Сделано"
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Infof("Rehash command from outer space: %s", spew.Sdump(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
case v.Text == fmt.Sprintf("%sver", j.C.CSign) || v.Text == fmt.Sprintf("%sversion", j.C.CSign):
|
||||||
|
var (
|
||||||
|
answer = fmt.Sprintf("Version %s", j.C.Version)
|
||||||
|
)
|
||||||
|
|
||||||
|
dest := v.Remote
|
||||||
|
|
||||||
|
if v.Type == "groupchat" {
|
||||||
|
dest = (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := j.Talk.Send(
|
||||||
|
xmpp.Chat{ //nolint:exhaustruct
|
||||||
|
Remote: dest,
|
||||||
|
Text: strings.TrimSpace(answer),
|
||||||
|
Type: v.Type,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
err = fmt.Errorf("unable to send message to %s: %w", v.Remote, err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package main
|
package jabber
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,9 +15,9 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки).
|
// ParseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки).
|
||||||
func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
func (j *Jabber) ParseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
||||||
lastServerActivity = time.Now().Unix()
|
j.LastServerActivity = time.Now().Unix()
|
||||||
|
|
||||||
switch v := e.(type) {
|
switch v := e.(type) {
|
||||||
// Сообщение в чяти
|
// Сообщение в чяти
|
||||||
@@ -30,20 +33,35 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
|
|
||||||
// Групповой чятик
|
// Групповой чятик
|
||||||
case "groupchat":
|
case "groupchat":
|
||||||
log.Debugf("Message from public chat: %s", v.Text)
|
var (
|
||||||
// dest := strings.SplitN(v.Remote, "/", 2)[0]
|
room = strings.SplitN(v.Remote, "/", 2)[0]
|
||||||
nick := strings.SplitN(v.Remote, "/", 2)[1]
|
nick = strings.SplitN(v.Remote, "/", 2)[1]
|
||||||
|
)
|
||||||
|
|
||||||
if nick == config.Jabber.Nick {
|
log.Debugf("Message from public chat: %s", v.Text)
|
||||||
|
|
||||||
|
if nick == j.GetBotNickFromRoomConfig(room) {
|
||||||
log.Debug("Skipping message from myself")
|
log.Debug("Skipping message from myself")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lastActivity = lastServerActivity
|
if err := j.Cmd(v); err != nil {
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.BunyChat(v); err != nil {
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j.LastActivity = j.LastServerActivity
|
||||||
|
|
||||||
if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" {
|
if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" {
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Приватный чятик
|
// Приватный чятик
|
||||||
@@ -53,7 +71,13 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// eleksir@jabber.ru/array.lan - это если мы работаем через ростер
|
// eleksir@jabber.ru/array.lan - это если мы работаем через ростер
|
||||||
log.Debugf("Private message: %s", v.Text)
|
log.Debugf("Private message: %s", v.Text)
|
||||||
|
|
||||||
lastActivity = lastServerActivity
|
if err := j.Cmd(v); err != nil {
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j.LastActivity = j.LastServerActivity
|
||||||
|
|
||||||
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
|
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
|
||||||
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика
|
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика
|
||||||
@@ -78,13 +102,13 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
log.Debug("Looks like IQ get query")
|
log.Debug("Looks like IQ get query")
|
||||||
|
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
if slices.Contains(roomsConnected, muc) {
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
iqStruct jabberSimpleIqGetQuery
|
iqStruct SimpleIqGetQuery
|
||||||
parseError bool
|
parseError bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -95,11 +119,11 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
case "jabber:iq:version":
|
case "jabber:iq:version":
|
||||||
log.Infof("Got IQ get request for version from %s", v.From)
|
log.Infof("Got IQ get request for version from %s", v.From)
|
||||||
|
|
||||||
if id, err := talk.IqVersionResponse(
|
if id, err := j.Talk.IqVersionResponse(
|
||||||
v,
|
v,
|
||||||
"buny-jabber-bot",
|
j.C.ExeName,
|
||||||
"1.с_чем-то",
|
j.C.Version,
|
||||||
"линупс",
|
(strings.SplitN(runtime.GOOS, "/", 2))[0],
|
||||||
); err != nil {
|
); err != nil {
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"unable to send version info to jabber server: id=%s, err=%w",
|
"unable to send version info to jabber server: id=%s, err=%w",
|
||||||
@@ -107,7 +131,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -118,14 +142,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
case "urn:xmpp:ping":
|
case "urn:xmpp:ping":
|
||||||
log.Infof("Got IQ get request for pong from %s", v.From)
|
log.Infof("Got IQ get request for pong from %s", v.From)
|
||||||
|
|
||||||
if id, err := talk.PingResponse(v); err != nil {
|
if id, err := j.Talk.PingResponse(v); err != nil {
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"unable to send pong to jabber server: id=%s, err=%w",
|
"unable to send pong to jabber server: id=%s, err=%w",
|
||||||
id,
|
id,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -137,14 +161,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
case "jabber:iq:last":
|
case "jabber:iq:last":
|
||||||
log.Infof("Got IQ get last activity time request from %s", v.From)
|
log.Infof("Got IQ get last activity time request from %s", v.From)
|
||||||
|
|
||||||
if id, err := talk.JabberIqLastResponse(v, lastActivity); err != nil {
|
if id, err := j.Talk.JabberIqLastResponse(v, j.LastActivity); err != nil {
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"unable to send last activity time to jabber server: id=%s, err=%w",
|
"unable to send last activity time to jabber server: id=%s, err=%w",
|
||||||
id,
|
id,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -160,10 +184,12 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
answer += "<feature var=\"urn:xmpp:time\" />"
|
answer += "<feature var=\"urn:xmpp:time\" />"
|
||||||
answer += "<feature var=\"urn:xmpp:ping\" />"
|
answer += "<feature var=\"urn:xmpp:ping\" />"
|
||||||
answer += "<feature var=\"jabber:iq:last\" />"
|
answer += "<feature var=\"jabber:iq:last\" />"
|
||||||
|
answer += "<feature var=\"http://jabber.org/protocol/caps\" />"
|
||||||
answer += "<feature var=\"http://jabber.org/protocol/disco#info\" />"
|
answer += "<feature var=\"http://jabber.org/protocol/disco#info\" />"
|
||||||
|
answer += "<feature var=\"http://jabber.org/protocol/muc\" />"
|
||||||
answer += "</query>"
|
answer += "</query>"
|
||||||
|
|
||||||
if id, err := talk.RawInformation(
|
if id, err := j.Talk.RawInformation(
|
||||||
v.To,
|
v.To,
|
||||||
v.From,
|
v.From,
|
||||||
v.ID,
|
v.ID,
|
||||||
@@ -176,7 +202,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -187,7 +213,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
case xmpp.XMPPNS_DISCO_ITEMS:
|
case xmpp.XMPPNS_DISCO_ITEMS:
|
||||||
log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From)
|
log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From)
|
||||||
|
|
||||||
if _, err := talk.ErrorServiceUnavailable(
|
if _, err := j.Talk.ErrorServiceUnavailable(
|
||||||
v,
|
v,
|
||||||
"http://jabber.org/protocol/disco#info",
|
"http://jabber.org/protocol/disco#info",
|
||||||
"http://jabber.org/protocol/commands",
|
"http://jabber.org/protocol/commands",
|
||||||
@@ -198,7 +224,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -218,21 +244,21 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
|
|
||||||
// Попробуем распарсить входящий запрос как urn:xmpp:time
|
// Попробуем распарсить входящий запрос как urn:xmpp:time
|
||||||
if parseError {
|
if parseError {
|
||||||
var iqStruct jabberTimeIqGetQuery
|
var iqStruct TimeIqGetQuery
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
switch iqStruct.Xmlns {
|
switch iqStruct.Xmlns {
|
||||||
case "urn:xmpp:time":
|
case "urn:xmpp:time":
|
||||||
log.Infof("Got IQ get time request from %s", v.From)
|
log.Infof("Got IQ get time request from %s", v.From)
|
||||||
|
|
||||||
if id, err := talk.UrnXmppTimeResponse(v, "+00:00"); err != nil {
|
if id, err := j.Talk.UrnXMPPTimeResponse(v, "+00:00"); err != nil {
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"unable to send urn:xmpp:time to jabber server: id=%s, err=%w",
|
"unable to send urn:xmpp:time to jabber server: id=%s, err=%w",
|
||||||
id,
|
id,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -252,14 +278,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
|
|
||||||
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub
|
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub
|
||||||
if parseError {
|
if parseError {
|
||||||
var iqStruct jabberPubsubIQGetQuery
|
var iqStruct PubsubIQGetQuery
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
switch iqStruct.Xmlns {
|
switch iqStruct.Xmlns {
|
||||||
case xmpp.XMPPNS_PUBSUB:
|
case xmpp.XMPPNS_PUBSUB:
|
||||||
log.Infof("Got IQ get pubsub request from %s, answer feature unimplemented", v.From)
|
log.Infof("Got IQ get pubsub request from %s, answer feature unimplemented", v.From)
|
||||||
|
|
||||||
if id, err := talk.ErrorNotImplemented(
|
if id, err := j.Talk.ErrorNotImplemented(
|
||||||
v,
|
v,
|
||||||
"http://jabber.org/protocol/pubsub#errors",
|
"http://jabber.org/protocol/pubsub#errors",
|
||||||
"subscribe",
|
"subscribe",
|
||||||
@@ -269,7 +295,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -290,19 +316,19 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// Предполагается, что такие респонсы должны приходить только для пинга без серверной оптимизации.
|
// Предполагается, что такие респонсы должны приходить только для пинга без серверной оптимизации.
|
||||||
// Хотя идейно мы не поддерживаем работу без серверной оптимизации, но на пинг ответим, нам несложно.
|
// Хотя идейно мы не поддерживаем работу без серверной оптимизации, но на пинг ответим, нам несложно.
|
||||||
if parseError {
|
if parseError {
|
||||||
var iqStruct jabberIqPing
|
var iqStruct IqPing
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
log.Debugf("Got IQ get request (actually, response) for MUC Self-Ping from %s", v.From)
|
log.Debugf("Got IQ get request (actually, response) for MUC Self-Ping from %s", v.From)
|
||||||
|
|
||||||
if id, err := talk.PingResponse(v); err != nil {
|
if id, err := j.Talk.PingResponse(v); err != nil {
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"unable to send pong to jabber server: id=%s, err=%w",
|
"unable to send pong to jabber server: id=%s, err=%w",
|
||||||
id,
|
id,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -319,28 +345,28 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
|
|
||||||
case xmpp.IQTypeResult:
|
case xmpp.IQTypeResult:
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
if slices.Contains(roomsConnected, muc) {
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
||||||
case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
case v.From == j.C.Jabber.Server && v.To == j.Talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||||
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
|
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
|
||||||
serverPingTimestampRx = time.Now().Unix() //nolint:wsl
|
j.ServerPingTimestampRx = time.Now().Unix() //nolint:wsl
|
||||||
|
|
||||||
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
|
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован.
|
||||||
case v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
case v.To == j.Talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||||
mucNameMatch := slices.Contains(roomsConnected, v.From)
|
mucNameMatch := slices.Contains(j.RoomsConnected, v.From)
|
||||||
|
|
||||||
if mucNameMatch {
|
if mucNameMatch {
|
||||||
log.Debugf("Got server-optimized MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
log.Debugf("Got server-optimized MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
||||||
} else {
|
} else {
|
||||||
mucNickMatch := false
|
mucNickMatch := false
|
||||||
|
|
||||||
for _, room := range roomsConnected {
|
for _, room := range j.RoomsConnected {
|
||||||
mucNick := fmt.Sprintf("%s/%s", room, config.Jabber.Nick)
|
mucNick := fmt.Sprintf("%s/%s", room, j.GetBotNickFromRoomConfig(room))
|
||||||
|
|
||||||
if v.From == mucNick {
|
if v.From == mucNick {
|
||||||
mucNickMatch = true
|
mucNickMatch = true
|
||||||
@@ -351,12 +377,89 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
|
|
||||||
if mucNickMatch {
|
if mucNickMatch {
|
||||||
log.Debugf("Got 2-nd stage MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
log.Debugf("Got 2-nd stage MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
||||||
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ
|
|
||||||
} else {
|
} else {
|
||||||
|
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ
|
||||||
log.Infof("Got unknown pong from %s to %s", v.From, v.To)
|
log.Infof("Got unknown pong from %s to %s", v.From, v.To)
|
||||||
log.Debug(spew.Sdump(e))
|
log.Debug(spew.Sdump(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подтверждение бана
|
||||||
|
case v.To == j.Talk.JID() && v.ID == "ban1":
|
||||||
|
mucNickMatch := false
|
||||||
|
|
||||||
|
// Если ник, которому предназначается сообщение, совпадает с ником, из конфига бота (глобального или для
|
||||||
|
// комнаты), то считаем, что мы есть в этой комнате. Потому что в противном случае в roomsConnected не
|
||||||
|
// будет искомой комнаты и мы не сможем составить ник бота, чтобы сравнить его с тем, кому адресовано
|
||||||
|
// уведомление.
|
||||||
|
for _, room := range j.RoomsConnected {
|
||||||
|
mucNick := fmt.Sprintf("%s/%s", room, j.GetBotNickFromRoomConfig(room))
|
||||||
|
|
||||||
|
if v.To == mucNick {
|
||||||
|
mucNickMatch = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Формально, ответ должен парситься как пустой result и совпадать с типом jabberSimpleIqGetQuery
|
||||||
|
// Ответ приходит с 2 xmlns, похоже, он парсится неправильно в go-xmpp.
|
||||||
|
// TODO: после исправления go-xmpp, надо исправить это тут
|
||||||
|
var iqStruct SimpleIqGetQuery
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
|
if iqStruct.Xmlns == "http://jabber.org/protocol/muc#admin" && iqStruct.Text == "" {
|
||||||
|
if mucNickMatch {
|
||||||
|
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ
|
||||||
|
log.Infof("Got ban successful from %s to %s", v.From, v.To)
|
||||||
|
} else {
|
||||||
|
log.Infof("Got ban successful from %s to %s, from room we are not in", v.From, v.To)
|
||||||
|
log.Debug(spew.Sdump(e))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Got strange ban successful message from %s to %s", v.From, v.To)
|
||||||
|
log.Debug(spew.Sdump(e))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Got strange ban successful message from %s to %s", v.From, v.To)
|
||||||
|
log.Debug(spew.Sdump(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ответ с результатом адресован нам.
|
||||||
|
case v.To == j.Talk.JID():
|
||||||
|
var softwareVersion IqResultSoftwareVersion
|
||||||
|
if err := xml.Unmarshal(v.Query, &softwareVersion); err == nil {
|
||||||
|
if softwareVersion.Os == "" {
|
||||||
|
log.Infof(
|
||||||
|
"Recieved software version query result for %s: software=%s version=%s",
|
||||||
|
v.From,
|
||||||
|
softwareVersion.Name,
|
||||||
|
softwareVersion.Version,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Infof(
|
||||||
|
"Recieved software version query result for %s: software=%s version=%s os=%s",
|
||||||
|
v.From,
|
||||||
|
softwareVersion.Name,
|
||||||
|
softwareVersion.Version,
|
||||||
|
softwareVersion.Os,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = j.BunySoftwareVersion(v, softwareVersion); err != nil {
|
||||||
|
log.Errorf("Unable to query client software version: %s", err)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Got an IQ result. Dunno how deal with it, discarding")
|
||||||
|
log.Debug(spew.Sdump(e))
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Info("Got an IQ result. Dunno how deal with it, discarding")
|
log.Info("Got an IQ result. Dunno how deal with it, discarding")
|
||||||
log.Debug(spew.Sdump(e))
|
log.Debug(spew.Sdump(e))
|
||||||
@@ -365,14 +468,14 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем
|
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем
|
||||||
case xmpp.IQTypeSet:
|
case xmpp.IQTypeSet:
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
if slices.Contains(roomsConnected, muc) {
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Got an IQ request for set something. Answer not implemented")
|
log.Info("Got an IQ request for set something. Answer not implemented")
|
||||||
|
|
||||||
if id, err := talk.ErrorNotImplemented(
|
if id, err := j.Talk.ErrorNotImplemented(
|
||||||
v,
|
v,
|
||||||
"http://jabber.org/protocol/commands",
|
"http://jabber.org/protocol/commands",
|
||||||
xmpp.IQTypeSet,
|
xmpp.IQTypeSet,
|
||||||
@@ -383,7 +486,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -395,18 +498,18 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
||||||
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
||||||
var (
|
var (
|
||||||
iqPingStruct jabberIqPing
|
iqPingStruct IqPing
|
||||||
parseError = true
|
parseError = true
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
||||||
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
||||||
if v.From == config.Jabber.Server {
|
if v.From == j.C.Jabber.Server {
|
||||||
msg := "Server announced that it can answer c2s ping, but gives us an error to such query, "
|
msg := "Server announced that it can answer c2s ping, but gives us an error to such query, "
|
||||||
msg += "fallback to keepalive whitespace pings"
|
msg += "fallback to keepalive whitespace pings"
|
||||||
log.Error(msg)
|
log.Error(msg)
|
||||||
|
|
||||||
serverCapsList.Set("urn:xmpp:ping", false)
|
j.ServerCapsList.Set("urn:xmpp:ping", false)
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("Got 'ping unsupported' message from: %s to: %s", v.From, v.To)
|
log.Errorf("Got 'ping unsupported' message from: %s to: %s", v.From, v.To)
|
||||||
}
|
}
|
||||||
@@ -420,27 +523,29 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
|
|
||||||
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
|
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
|
||||||
if parseError {
|
if parseError {
|
||||||
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable
|
var iqErrorCancelNotAcceptable IqErrorCancelNotAcceptable
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
||||||
if v.To == talk.JID() {
|
if v.To == j.Talk.JID() {
|
||||||
nick := strings.SplitN(v.From, "/", 2)[1]
|
nick := strings.SplitN(v.From, "/", 2)[1]
|
||||||
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
|
||||||
if slices.Contains(roomsConnected, iqErrorCancelNotAcceptable.By) && nick == config.Jabber.Nick {
|
if slices.Contains(j.RoomsConnected, iqErrorCancelNotAcceptable.By) &&
|
||||||
|
nick == j.GetBotNickFromRoomConfig(room) {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
"Got Iq error message from: %s to: %s. Looks like i'm not in MUC anymore",
|
"Got Iq error message from: %s to: %s. Looks like i'm not in MUC anymore",
|
||||||
v.From, v.To,
|
v.From, v.To,
|
||||||
)
|
)
|
||||||
|
|
||||||
time.Sleep(time.Duration(config.Jabber.MucRejoinDelay) * time.Second)
|
time.Sleep(time.Duration(j.C.Jabber.MucRejoinDelay) * time.Second)
|
||||||
|
|
||||||
if _, err := talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil {
|
if _, err := j.Talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil {
|
||||||
err := fmt.Errorf(
|
err := fmt.Errorf(
|
||||||
"looks like connection to server also lost err=%w",
|
"looks like connection to server also lost err=%w",
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
||||||
gTomb.Kill(err)
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -476,8 +581,8 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// Смена статуса участника
|
// Смена статуса участника
|
||||||
case xmpp.Presence:
|
case xmpp.Presence:
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
if slices.Contains(roomsConnected, muc) {
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,39 +593,114 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// клиента забанят из-за спама "вошёл-вышел".
|
// клиента забанят из-за спама "вошёл-вышел".
|
||||||
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
|
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
|
||||||
if v.Type == "unavailable" {
|
if v.Type == "unavailable" {
|
||||||
|
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть
|
||||||
|
j.RoomPresences.Delete(v.From)
|
||||||
|
|
||||||
log.Error("Presence notification - looks like another instance of client leaves room")
|
log.Error("Presence notification - looks like another instance of client leaves room")
|
||||||
|
|
||||||
if slices.Contains(roomsConnected, v.From) {
|
if slices.Contains(j.RoomsConnected, v.From) {
|
||||||
go joinMuc(v.From)
|
go j.JoinMuc(v.From)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
|
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
|
||||||
v.Type, v.From, v.Show, v.Status)
|
v.Type, v.From, v.Show, v.Status)
|
||||||
log.Errorf(spew.Sdump(v))
|
log.Error(spew.Sdump(v))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s",
|
log.Infof(
|
||||||
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID)
|
"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,
|
||||||
|
)
|
||||||
|
|
||||||
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
nick := strings.SplitN(v.From, "/", 2)[1]
|
||||||
|
|
||||||
|
if nick == "" {
|
||||||
|
log.Infof("Presence stanza contains incorrect from attribute: %s", v.From)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Это наш собственный Presence
|
// Это наш собственный Presence
|
||||||
if v.Show == "" && v.Status == "" {
|
if v.Show == "" && v.Status == "" {
|
||||||
if from := strings.Split(v.From, "/"); from[1] == config.Jabber.Nick {
|
if nick == j.GetBotNickFromRoomConfig(room) {
|
||||||
roomsConnected = append(roomsConnected, from[0])
|
j.RoomsConnected = append(j.RoomsConnected, room)
|
||||||
|
// На всякий случай дедуплицируем список комнат, к которым мы заджойнились.
|
||||||
|
sort.Strings(j.RoomsConnected)
|
||||||
|
slices.Compact(j.RoomsConnected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := buny(v); err != nil {
|
switch v.Role {
|
||||||
gTomb.Kill(err)
|
// Участник ушёл
|
||||||
|
case "none":
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSONStrings := InterfaceToStringSlice(presenceJSONInterface)
|
||||||
|
|
||||||
return
|
var newPresenceJSONStrings []string
|
||||||
|
|
||||||
|
for _, presenceJSONstring := range presenceJSONStrings {
|
||||||
|
var p xmpp.Presence
|
||||||
|
_ = json.Unmarshal([]byte(presenceJSONstring), &p)
|
||||||
|
|
||||||
|
if p.From == v.From {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONstring)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.RoomPresences.Set(room, newPresenceJSONStrings)
|
||||||
|
}
|
||||||
|
// Участник пришёл
|
||||||
|
default:
|
||||||
|
var (
|
||||||
|
presenceJSONStrings []string
|
||||||
|
newPresenceJSONStrings []string
|
||||||
|
presenceJSONBytes []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSONStrings = InterfaceToStringSlice(presenceJSONInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, presenceJSONString := range presenceJSONStrings {
|
||||||
|
var p xmpp.Presence
|
||||||
|
_ = json.Unmarshal([]byte(presenceJSONString), &p)
|
||||||
|
|
||||||
|
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat)
|
||||||
|
// просто замещаем его.
|
||||||
|
if p.From == v.From {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONString)
|
||||||
|
}
|
||||||
|
|
||||||
|
presenceJSONBytes, _ = json.Marshal(v) //nolint:errchkjson
|
||||||
|
newPresenceJSONStrings = append(newPresenceJSONStrings, string(presenceJSONBytes))
|
||||||
|
j.RoomPresences.Set(room, newPresenceJSONStrings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате.
|
||||||
|
// По правилам, мы можем что-то делать, только после того, как нам прилетит наш собственный presence, это
|
||||||
|
// значит, что мы вошли в комнату.
|
||||||
|
if slices.Contains(j.RoomsConnected, room) {
|
||||||
|
if v.Affiliation != "outcast" {
|
||||||
|
if err := j.BunyPresense(v); err != nil {
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info"
|
// Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info"
|
||||||
case xmpp.DiscoResult:
|
case xmpp.DiscoResult:
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
if slices.Contains(roomsConnected, muc) {
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,10 +712,10 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
// Конкретно сейчас нас интересует только поддержка c2s ping
|
// Конкретно сейчас нас интересует только поддержка c2s ping
|
||||||
for _, feature := range v.Features {
|
for _, feature := range v.Features {
|
||||||
log.Debugf("Server %s announced that it supports feature: %s", v.From, feature)
|
log.Debugf("Server %s announced that it supports feature: %s", v.From, feature)
|
||||||
serverCapsList.Set(feature, true)
|
j.ServerCapsList.Set(feature, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverCapsQueried = true
|
j.ServerCapsQueried = true
|
||||||
|
|
||||||
case "conference":
|
case "conference":
|
||||||
mucCaps := make(map[string]bool)
|
mucCaps := make(map[string]bool)
|
||||||
@@ -545,7 +725,7 @@ func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|||||||
mucCaps[feature] = true //nolint:wsl
|
mucCaps[feature] = true //nolint:wsl
|
||||||
}
|
}
|
||||||
|
|
||||||
mucCapsList.Set(v.From, mucCaps)
|
j.MucCapsList.Set(v.From, mucCaps)
|
||||||
|
|
||||||
case "pubsub":
|
case "pubsub":
|
||||||
log.Debugf("PubSub component %s reply to disco#info, skipping", v.From)
|
log.Debugf("PubSub component %s reply to disco#info, skipping", v.From)
|
||||||
82
internal/jabber/main_loop.go
Normal file
82
internal/jabber/main_loop.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyLoop - основной цикл программы.
|
||||||
|
func (j *Jabber) MyLoop() error {
|
||||||
|
for {
|
||||||
|
// Зададим начальное значение глобальным переменным
|
||||||
|
j.ServerPingTimestampRx = 0
|
||||||
|
j.ServerPingTimestampTx = 0
|
||||||
|
j.RoomsConnected = make([]string, 1)
|
||||||
|
j.LastActivity = 0
|
||||||
|
j.LastServerActivity = 0
|
||||||
|
j.LastMucActivity = NewCollection()
|
||||||
|
j.ServerCapsQueried = false
|
||||||
|
j.ServerCapsList = NewCollection()
|
||||||
|
j.MucCapsList = NewCollection()
|
||||||
|
j.ServerPingTimestampTx = 0
|
||||||
|
j.ServerPingTimestampRx = 0
|
||||||
|
j.RoomPresences = NewCollection()
|
||||||
|
|
||||||
|
// Установим коннект
|
||||||
|
if err := j.EstablishConnection(); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ServerPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен.
|
||||||
|
|
||||||
|
// Тыкаем сервер палочкой, проверяем, что коннект жив и вываливаемся из mainLoop, если он не жив.
|
||||||
|
j.GTomb.Go(
|
||||||
|
func() error {
|
||||||
|
err := j.ProbeServerLiveness()
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Тыкаем muc-и палочкой, проверяем, что они живы и вываливаемся из mainLoop, если пинги пропали.
|
||||||
|
// Если пинги до комнаты пропали, то это фактически значит, что либо сервер потерял связь с MUC-компонентом,
|
||||||
|
// либо у нас какой-то wire error.
|
||||||
|
// j.GTomb.Go() тут зовётся внутри на каждую комнату, если у нас ошибка, эта горутинка рано или поздно выйдет.
|
||||||
|
go j.ProbeMUCLiveness()
|
||||||
|
|
||||||
|
// Гребём ивенты...
|
||||||
|
for {
|
||||||
|
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например)
|
||||||
|
// грести не надо
|
||||||
|
if j.Shutdown {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !j.IsConnected {
|
||||||
|
// Tight loop - это наверно не очень хорошо, думаю, ничего страшного не будет, если мы поспим 100мс.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вынимаем ивент из "провода"
|
||||||
|
chat, err := j.Talk.Recv()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to get recieve data from socket: %s", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ParseEvent(chat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
56
internal/jabber/norm_string.go
Normal file
56
internal/jabber/norm_string.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Переменные для простенького нормализатора текста.
|
||||||
|
var (
|
||||||
|
// Знаки препинания раз.
|
||||||
|
pMarks = []string{".", ",", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "{", "}", "<", ">", "[", "]", "\\"}
|
||||||
|
|
||||||
|
// Знаки препинания два-с.
|
||||||
|
pMarks2 = []string{"-", "_", "+", "=", ":", ";", "'", "`", "~", "\""}
|
||||||
|
|
||||||
|
// Символы новой строки.
|
||||||
|
newLines = []string{"\n", "\r", "\n\r", "\r\n"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Нормализует текстовый буфер. Убирает начальные и конечные пробельные символы, заменяет сносы строк пробелами, удаляет
|
||||||
|
// знаки препинания, схлопывает повторяющиеся пробелы в один символ.
|
||||||
|
func nString(buf string) string {
|
||||||
|
// Убираем пробелы вначале текстового буфера и в конце.
|
||||||
|
buf = strings.Trim(buf, "\n\r\t ")
|
||||||
|
|
||||||
|
// Убираем знаки препинания.
|
||||||
|
for _, pMark := range pMarks {
|
||||||
|
buf = strings.ReplaceAll(buf, pMark, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pMark := range pMarks2 {
|
||||||
|
buf = strings.ReplaceAll(buf, pMark, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Замещаем сносы строки пробелами.
|
||||||
|
for _, newline := range newLines {
|
||||||
|
buf = strings.ReplaceAll(buf, newline, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Схлопываем повторяющиеся пробелы.
|
||||||
|
buf = regexp.MustCompile(`\s+`).ReplaceAllString(buf, " ")
|
||||||
|
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обёртка для nString, возвращает нормализованную строку в нижнем регистре.
|
||||||
|
func nStringLower(buf string) string { //nolint:unused
|
||||||
|
return strings.ToLower(nString(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обёртка для nString, возвращает нормализованную строку в верхнем регистре.
|
||||||
|
func nStringUpper(buf string) string {
|
||||||
|
return strings.ToUpper(nString(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
36
internal/jabber/query_version.go
Normal file
36
internal/jabber/query_version.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuerySoftwareVersion запрошивает версию и название клиента.
|
||||||
|
func (j *Jabber) QuerySoftwareVersion(jid string) (string, error) {
|
||||||
|
var (
|
||||||
|
id string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if id, err = j.Talk.RawInformationQuery(
|
||||||
|
j.Talk.JID(),
|
||||||
|
jid,
|
||||||
|
uuid.New().String(),
|
||||||
|
xmpp.IQTypeGet,
|
||||||
|
"jabber:iq:version",
|
||||||
|
"",
|
||||||
|
); err != nil {
|
||||||
|
err = fmt.Errorf(
|
||||||
|
"unable to query software version of jid=%s err=%w",
|
||||||
|
jid,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Infof("Query software version of %s", jid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
280
internal/jabber/read_config.go
Normal file
280
internal/jabber/read_config.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hjson/hjson-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
|
||||||
|
// нет.
|
||||||
|
func (j *Jabber) ReadConfig() error { //nolint:gocognit,gocyclo
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
executablePath string
|
||||||
|
configLoaded = false
|
||||||
|
)
|
||||||
|
|
||||||
|
executablePath, err = os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("unable to get current executable path: %w", err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
||||||
|
|
||||||
|
locations := []string{
|
||||||
|
"~/.buny-jabber-bot.json",
|
||||||
|
"~/buny-jabber-bot.json",
|
||||||
|
"/etc/buny-jabber-bot.json",
|
||||||
|
configJSONPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, location := range locations {
|
||||||
|
fileInfo, err := os.Stat(location)
|
||||||
|
|
||||||
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||||
|
if fileInfo.Size() > 2777216 {
|
||||||
|
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := os.ReadFile(location)
|
||||||
|
|
||||||
|
// Не удалось прочитать, попробуем следующего кандидата.
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
|
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Не удалось преобразовать map-ку в json
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil {
|
||||||
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Значения для Jabber-клиента
|
||||||
|
if sampleConfig.Jabber.Server == "" {
|
||||||
|
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 = 5222
|
||||||
|
|
||||||
|
if sampleConfig.Jabber.Ssl {
|
||||||
|
if !sampleConfig.Jabber.StartTLS {
|
||||||
|
sampleConfig.Jabber.Port = 5223
|
||||||
|
|
||||||
|
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 {
|
||||||
|
sampleConfig.Jabber.StartTLS = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sampleConfig.Jabber.Ssl || !sampleConfig.Jabber.StartTLS {
|
||||||
|
sampleConfig.Jabber.SslVerify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Jabber.InsecureAllowUnencryptedAuth = false, если не задан
|
||||||
|
|
||||||
|
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 == "" {
|
||||||
|
return errors.New("jabber nick is not defined in config, quitting") //nolint:goerr113
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Jabber.Resource == "" {
|
||||||
|
sampleConfig.Jabber.Resource = "bunyPresense bot"
|
||||||
|
|
||||||
|
log.Info("Jabber resource not defined in config, using bunyPresense 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 не задан, то авторизации не будет
|
||||||
|
|
||||||
|
// Если не задано ни одного мастера, то бот сам себе мастер
|
||||||
|
if len(sampleConfig.Jabber.BotMasters) == 0 {
|
||||||
|
sampleConfig.Jabber.BotMasters[0] = sampleConfig.Jabber.User
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нам бот нужен в каких-то чат-румах, а не "просто так"
|
||||||
|
if len(sampleConfig.Jabber.Channels) < 1 {
|
||||||
|
return errors.New("no jabber channels/rooms defined in config, quitting") //nolint:goerr113
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, channel := range sampleConfig.Jabber.Channels {
|
||||||
|
if channel.Name == "" {
|
||||||
|
return errors.New("no \"name\" entry in jabber channel config") //nolint:goerr113
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.Nick == "" {
|
||||||
|
sampleConfig.Jabber.Channels[n].Nick = sampleConfig.Jabber.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel.Password может быть пустым, тогда пароля нет
|
||||||
|
// channel.Bayes.Enabled будет false, если не проставлен
|
||||||
|
|
||||||
|
if channel.Bayes.MinLength < 40 {
|
||||||
|
channel.Bayes.MinLength = 40
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel.Bayes.MinWords < 8 {
|
||||||
|
channel.Bayes.MinWords = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
switch channel.Bayes.DefaultAction {
|
||||||
|
case "kick":
|
||||||
|
case "ban":
|
||||||
|
case "devoice":
|
||||||
|
default:
|
||||||
|
channel.Bayes.DefaultAction = "log"
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel.AllCaps.Enabled будет false, если не указан
|
||||||
|
if channel.AllCaps.MinLength < 10 {
|
||||||
|
channel.AllCaps.MinLength = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
switch channel.AllCaps.DefaultAction {
|
||||||
|
case "kick":
|
||||||
|
case "ban":
|
||||||
|
case "devoice":
|
||||||
|
default:
|
||||||
|
channel.AllCaps.DefaultAction = "log"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если список фраз с которыми стартует бот пустой, вносим в него 1 запись с пустой строкой
|
||||||
|
if len(sampleConfig.Jabber.StartupStatus) == 0 {
|
||||||
|
sampleConfig.Jabber.StartupStatus[0] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если список статусов, с которыми работает бот пустой, вносим в него 1 запись с пустой строкой
|
||||||
|
if len(sampleConfig.Jabber.RuntimeStatus.Text) == 0 {
|
||||||
|
sampleConfig.Jabber.RuntimeStatus.Text[0] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если sampleConfig.Jabber.RuntimeStatus.RotationTime не задан, то он равен 0
|
||||||
|
// Если sampleConfig.Jabber.RuntimeStatus.RotationSplayTime не задан, то он равен 0
|
||||||
|
|
||||||
|
// Если sampleConfig.Jabber.BanDelay не задан, то он равен 0
|
||||||
|
// Если sampleConfig.Jabber.BanPhrasesEnable не задан, то он false
|
||||||
|
|
||||||
|
// Если список фраз, с которыми банят пустой, то вносим в него одну позицию с пустой строкой
|
||||||
|
if len(sampleConfig.Jabber.BanPhrases) == 0 {
|
||||||
|
sampleConfig.Jabber.BanPhrases[0] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.CSign == "" {
|
||||||
|
sampleConfig.CSign = "!"
|
||||||
|
}
|
||||||
|
|
||||||
|
if sampleConfig.Loglevel == "" {
|
||||||
|
sampleConfig.Loglevel = "info"
|
||||||
|
|
||||||
|
log.Info("loglevel not defined in config, using info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleConfig.Log = "" if not set
|
||||||
|
|
||||||
|
j.C = sampleConfig
|
||||||
|
configLoaded = true
|
||||||
|
|
||||||
|
log.Infof("Using %s as config file", location)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !configLoaded {
|
||||||
|
return errors.New("config was not loaded") //nolint:goerr113
|
||||||
|
}
|
||||||
|
|
||||||
|
return err //nolint:wrapcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
62
internal/jabber/sig_handler.go
Normal file
62
internal/jabber/sig_handler.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (j *Jabber) SigHandler() error {
|
||||||
|
log.Debug("Installing signal handler")
|
||||||
|
|
||||||
|
for s := range j.SigChan {
|
||||||
|
switch s {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
log.Infoln("Got SIGINT, quitting")
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
log.Infoln("Got SIGTERM, quitting")
|
||||||
|
case syscall.SIGQUIT:
|
||||||
|
log.Infoln("Got SIGQUIT, quitting")
|
||||||
|
|
||||||
|
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Чтобы не срать в логи ошибками, проставим shutdown state приложения в true.
|
||||||
|
j.Shutdown = true
|
||||||
|
|
||||||
|
if j.IsConnected && !j.Shutdown {
|
||||||
|
log.Debug("Try to set our presence to Unavailable and status to Offline")
|
||||||
|
|
||||||
|
// Вот тут понадобится коллекция известных пользователей, чтобы им разослать presence, что бот свалил в
|
||||||
|
// offline. Пока за неимением лучшего сообщим об этом самим себе.
|
||||||
|
for _, room := range j.RoomsConnected {
|
||||||
|
if _, err := j.Talk.SendPresence(
|
||||||
|
xmpp.Presence{ //nolint:exhaustruct
|
||||||
|
To: room,
|
||||||
|
Status: "Offline",
|
||||||
|
Type: "unavailable",
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
log.Infof("Unable to send presence to jabber server: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// И закрываем соединение.
|
||||||
|
log.Infoln("Closing connection to jabber server")
|
||||||
|
|
||||||
|
if err := j.Talk.Close(); err != nil {
|
||||||
|
log.Infof("Unable to close connection to jabber server: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
80
internal/jabber/squash.go
Normal file
80
internal/jabber/squash.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Squash банит указанный jid в указанной комнате.
|
||||||
|
// reasonEnable указывает, надо ли писать дату автобана в банлисте в поле reason (это единственная причина, в которую
|
||||||
|
// умеет бот).
|
||||||
|
func (j *Jabber) Squash(room, jid string, reasonEnable bool, vType string) (string, error) {
|
||||||
|
var (
|
||||||
|
id string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if j.C.Jabber.BanPhrasesEnable {
|
||||||
|
phrase := RandomPhrase(j.C.Jabber.BanPhrases)
|
||||||
|
|
||||||
|
if _, err = j.Talk.Send(
|
||||||
|
xmpp.Chat{ //nolint:exhaustruct
|
||||||
|
Remote: room,
|
||||||
|
Text: phrase,
|
||||||
|
Type: vType,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
err = fmt.Errorf("unable to send phrase to room %s: %w", room, err)
|
||||||
|
|
||||||
|
// Здесь возвращаем nil, т.к. за нас ошибку залоггирует код выше.
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://xmpp.org/extensions/xep-0045.html#ban баним вот таким сообщением
|
||||||
|
ban := "<item affiliation='outcast' jid='" + jid + "'>"
|
||||||
|
|
||||||
|
if reasonEnable {
|
||||||
|
var t = time.Now()
|
||||||
|
ban += fmt.Sprintf(
|
||||||
|
"<reason>autoban at %04d.%02d.%02d %02d:%02d:%02d</reason>",
|
||||||
|
t.Year(),
|
||||||
|
t.Month(),
|
||||||
|
t.Day(),
|
||||||
|
t.Hour(),
|
||||||
|
t.Minute(),
|
||||||
|
t.Second(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ban += "<reason />"
|
||||||
|
}
|
||||||
|
|
||||||
|
ban += "</item>"
|
||||||
|
|
||||||
|
// Выжидаем некоторое время перед баном. А то можно настолько рано забанить, что сервер не внесёт злодея в банлист
|
||||||
|
// комнаты и пришлёт affiliation: none вместо affiliation: outcast.
|
||||||
|
if j.C.Jabber.BanDelay > 0 {
|
||||||
|
time.Sleep(time.Duration(j.C.Jabber.BanDelay) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, err = j.Talk.RawInformationQuery(
|
||||||
|
j.Talk.JID(),
|
||||||
|
room,
|
||||||
|
"ban1",
|
||||||
|
xmpp.IQTypeSet,
|
||||||
|
"http://jabber.org/protocol/muc#admin",
|
||||||
|
ban,
|
||||||
|
); err != nil {
|
||||||
|
err = fmt.Errorf(
|
||||||
|
"unable to ban user: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
215
internal/jabber/types.go
Normal file
215
internal/jabber/types.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
"gopkg.in/tomb.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MyConfig прототип структурки с конфигом.
|
||||||
|
type MyConfig struct {
|
||||||
|
Jabber struct {
|
||||||
|
Server string `json:"server,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Ssl bool `json:"ssl,omitempty"`
|
||||||
|
StartTLS bool `json:"starttls,omitempty"`
|
||||||
|
SslVerify bool `json:"ssl_verify,omitempty"`
|
||||||
|
InsecureAllowUnencryptedAuth bool `json:"insecureallowunencryptedauth,omitempty"`
|
||||||
|
ConnectionTimeout int64 `json:"connection_timeout,omitempty"`
|
||||||
|
ReconnectDelay int64 `json:"reconnect_delay,omitempty"`
|
||||||
|
ServerPingDelay int64 `json:"server_ping_delay,omitempty"`
|
||||||
|
MucPingDelay int64 `json:"muc_ping_delay,omitempty"`
|
||||||
|
MucRejoinDelay int64 `json:"muc_rejoin_delay,omitempty"`
|
||||||
|
PingSplayDelay int64 `json:"ping_splay_delay,omitempty"`
|
||||||
|
Nick string `json:"nick,omitempty"`
|
||||||
|
Resource string `json:"resource,omitempty"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
BotMasters []string `json:"bot_masters,omitempty"`
|
||||||
|
Channels []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Nick string `json:"nick,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Bayes struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
MinWords int64 `json:"min_words,omitempty"`
|
||||||
|
MinLength int `json:"min_length,omitempty"`
|
||||||
|
DefaultAction string `json:"default_action,omitempty"`
|
||||||
|
} `json:"bayes,omitempty"`
|
||||||
|
AllCaps struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
MinLength int `json:"min_length,omitempty"`
|
||||||
|
DefaultAction string `json:"default_action,omitempty"`
|
||||||
|
} `json:"all_caps,omitempty"`
|
||||||
|
} `json:"channels"`
|
||||||
|
StartupStatus []string `json:"startup_status,omitempty"`
|
||||||
|
RuntimeStatus struct {
|
||||||
|
Text []string `json:"text,omitempty"`
|
||||||
|
RotationTime int64 `json:"rotation_time,omitempty"`
|
||||||
|
RotationSplayTime int64 `json:"rotation_splay_time,omitempty"`
|
||||||
|
} `json:"runtime_status,omitempty"`
|
||||||
|
BanDelay int64 `json:"ban_delay,omitempty"`
|
||||||
|
BanPhrasesEnable bool `json:"ban_phrases_enable,omitempty"`
|
||||||
|
BanPhrases []string `json:"ban_phrases,omitempty"`
|
||||||
|
} `json:"jabber,omitempty"`
|
||||||
|
|
||||||
|
CSign string `json:"csign,omitempty"`
|
||||||
|
Loglevel string `json:"loglevel,omitempty"`
|
||||||
|
Log string `json:"log,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
ExeName string `json:"exe_name,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,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyBlackList прототип структурки с чёрным списком jid-ов.
|
||||||
|
type MyBlackList struct {
|
||||||
|
Blacklist []struct {
|
||||||
|
RoomName string `json:"room_name,omitempty"`
|
||||||
|
ReasonEnable bool `json:"reason_enable,omitempty"`
|
||||||
|
JidRe []string `json:"jid_re,omitempty"`
|
||||||
|
NickRe []string `json:"nick_re,omitempty"`
|
||||||
|
PhraseRe []string `json:"phrase_re,omitempty"`
|
||||||
|
UserAgent []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Os string `json:"os,omitempty"`
|
||||||
|
} `json:"user_agent,omitempty"`
|
||||||
|
} `json:"blacklist,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jabber основная структура-объект, содержащая стейты и проч.
|
||||||
|
type Jabber struct {
|
||||||
|
// C - конфиг, как он распарсился из конфиг-файла.
|
||||||
|
C MyConfig
|
||||||
|
|
||||||
|
// WhiteList - структурка с безусловно разрешёнными jid-ами
|
||||||
|
WhiteList MyWhiteList
|
||||||
|
|
||||||
|
// BlackList - структурка с запрещёнными по регуляркам фразами, никами, jid-ами.
|
||||||
|
BlackList MyBlackList
|
||||||
|
|
||||||
|
// Опции подключения к xmpp-серверу.
|
||||||
|
Options *xmpp.Options
|
||||||
|
|
||||||
|
// gTomb пул активных горутин.
|
||||||
|
GTomb tomb.Tomb
|
||||||
|
|
||||||
|
// Talk основная структурка xmpp-клиента.
|
||||||
|
Talk *xmpp.Client
|
||||||
|
|
||||||
|
// sync.Map-ка с капабилити сервера.
|
||||||
|
ServerCapsList *Collection
|
||||||
|
|
||||||
|
// ServerCapsQueried показывает, были ли запрошены capabilities сервера.
|
||||||
|
ServerCapsQueried bool
|
||||||
|
|
||||||
|
// Время последней активности, нужно для c2s пингов - посылаем пинги, только если давненько ничего не приходило с
|
||||||
|
// сервера.
|
||||||
|
LastServerActivity int64
|
||||||
|
|
||||||
|
// Время, когда был отправлен c2s ping.
|
||||||
|
ServerPingTimestampTx int64
|
||||||
|
|
||||||
|
// Время, когда был принят s2c pong.
|
||||||
|
ServerPingTimestampRx int64
|
||||||
|
|
||||||
|
// Время последней активности, нужно для jabber:iq:last.
|
||||||
|
LastActivity int64
|
||||||
|
|
||||||
|
// sync.Map-ка с комнатами и их capability.
|
||||||
|
MucCapsList *Collection
|
||||||
|
|
||||||
|
// Время последней активности MUC-ов, нужно для пингов - посылаем пинги, только если давненько ничего не приходило из
|
||||||
|
// muc-ов.
|
||||||
|
LastMucActivity *Collection
|
||||||
|
|
||||||
|
// Список комнат, в которых находится бот.
|
||||||
|
RoomsConnected []string
|
||||||
|
|
||||||
|
// sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json).
|
||||||
|
RoomPresences *Collection
|
||||||
|
|
||||||
|
// Канал, по котором приходят сообщения о том, что ОС отправила некие сигналы процессу.
|
||||||
|
SigChan chan os.Signal
|
||||||
|
|
||||||
|
// Индиктор того, что процесс завершается.
|
||||||
|
Shutdown bool
|
||||||
|
|
||||||
|
// Индиктор того, что соединение установлено.
|
||||||
|
IsConnected bool
|
||||||
|
|
||||||
|
// Индикатор того, что соединение в процессе достукивания до сервера.
|
||||||
|
Connecting bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleIqGetQuery прототип структурки для разбора запросов xmpp discovery query, например,
|
||||||
|
// https://xmpp.org/extensions/xep-0030.html#example-18 .
|
||||||
|
type SimpleIqGetQuery struct {
|
||||||
|
XMLName xml.Name `xml:"query"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Node string `xml:"node,attr,omitempty"` // для xmlns="http://jabber.org/protocol/disco#items"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubsubIQGetQuery прототип структурки для разбора запросов xmpp pubsub.
|
||||||
|
type PubsubIQGetQuery struct {
|
||||||
|
XMLName xml.Name `xml:"pubsub"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Items struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Node string `xml:"node,attr"`
|
||||||
|
MaxItems string `xml:"max_items,attr"`
|
||||||
|
} `xml:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
|
||||||
|
// https://xmpp.org/extensions/xep-0202.html
|
||||||
|
type TimeIqGetQuery struct {
|
||||||
|
// <time xmlns="urn:xmpp:time"/>
|
||||||
|
XMLName xml.Name `xml:"time"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IqPing прототип структурки для разбора IQ запросов на пинг клиента, https://xmpp.org/extensions/xep-0199.html
|
||||||
|
type IqPing struct {
|
||||||
|
XMLName xml.Name `xml:"ping"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IqErrorCancelNotAcceptable прототип структурки для разбора IQ ответов, когда сервис (сервер, клиент, etc) не
|
||||||
|
// может или не хочет принимать наш iq-запрос https://xmpp.org/extensions/xep-0099.html
|
||||||
|
type IqErrorCancelNotAcceptable struct {
|
||||||
|
XMLName xml.Name `xml:"error"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
By string `xml:"by,attr"`
|
||||||
|
NotAcceptable struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
} `xml:"not-acceptable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IqResultSoftwareVersion прототип структурки для разбора IQ ответов на запрос о названии и версии клиентского ПО.
|
||||||
|
type IqResultSoftwareVersion struct {
|
||||||
|
XMLName xml.Name `xml:"query"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Version string `xml:"version"`
|
||||||
|
Os string `xml:"os,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
435
internal/jabber/util.go
Normal file
435
internal/jabber/util.go
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/eleksir/go-xmpp"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomPhrase выдаёт одну рандомную фразу из даденного списка фраз.
|
||||||
|
func RandomPhrase(list []string) string {
|
||||||
|
phrase := ""
|
||||||
|
|
||||||
|
if listLen := len(list); listLen > 0 {
|
||||||
|
phrase = list[rand.Intn(listLen)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return phrase
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceToStringSlice превращает данный интерфейс в слайс строк.
|
||||||
|
// Если может, конечно :) .
|
||||||
|
func InterfaceToStringSlice(iface interface{}) []string {
|
||||||
|
var mySlice []string
|
||||||
|
|
||||||
|
// А теперь мы начинаем дурдом, нам надо превратить ёбанный interface{} в []string
|
||||||
|
// Поскольку interface{} может быть чем угодно, перестрахуемся
|
||||||
|
if reflect.TypeOf(iface).Kind() == reflect.Slice {
|
||||||
|
shit := reflect.ValueOf(iface)
|
||||||
|
|
||||||
|
for i := 0; i < shit.Len(); i++ {
|
||||||
|
mySlice = append(mySlice, fmt.Sprint(shit.Index(i)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mySlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRealJIDfromNick достаёт из запомненных presence-ов по даденному nick-у real jid с resource-ом. Nick должен
|
||||||
|
// содержать имя конфы, откуда участник.
|
||||||
|
func (j *Jabber) GetRealJIDfromNick(fullNick string) string {
|
||||||
|
var p xmpp.Presence
|
||||||
|
|
||||||
|
room := (strings.SplitN(fullNick, "/", 2))[0]
|
||||||
|
|
||||||
|
// Достанем presence участника
|
||||||
|
presenceJSONInterface, present := j.RoomPresences.Get(room)
|
||||||
|
|
||||||
|
// Никого нет дома
|
||||||
|
if !present {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
presenceJSONStrings := InterfaceToStringSlice(presenceJSONInterface)
|
||||||
|
|
||||||
|
for _, presepresenceJSONString := range presenceJSONStrings {
|
||||||
|
_ = json.Unmarshal([]byte(presepresenceJSONString), &p)
|
||||||
|
|
||||||
|
if p.From == fullNick {
|
||||||
|
return p.JID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBotNickFromRoomConfig достаёт из настроек комнаты короткий ник бота, либо берёт значение из конфига, если ник в
|
||||||
|
// настройках комнаты не задан. Короткий ник не содержит название комнаты.
|
||||||
|
func (j *Jabber) GetBotNickFromRoomConfig(room string) string {
|
||||||
|
for _, roomStruct := range j.C.Jabber.Channels {
|
||||||
|
if roomStruct.Name == room {
|
||||||
|
return roomStruct.Nick
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.C.Jabber.Nick
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига.
|
||||||
|
func (j *Jabber) RotateStatus(room string) error {
|
||||||
|
for {
|
||||||
|
// TODO: Переделать на ticker-ы
|
||||||
|
totalSleepTime := time.Duration(j.C.Jabber.RuntimeStatus.RotationTime) * time.Second
|
||||||
|
totalSleepTime += time.Duration(j.C.Jabber.RuntimeStatus.RotationSplayTime) * time.Second
|
||||||
|
|
||||||
|
for {
|
||||||
|
status := RandomPhrase(j.C.Jabber.RuntimeStatus.Text)
|
||||||
|
log.Debugf("Set status for MUC: %s to: %s", room, status)
|
||||||
|
|
||||||
|
var p xmpp.Presence
|
||||||
|
|
||||||
|
if room != "" {
|
||||||
|
p = xmpp.Presence{ //nolint:exhaustruct
|
||||||
|
To: room,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p = xmpp.Presence{ //nolint:exhaustruct
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := j.Talk.SendPresence(p); err != nil {
|
||||||
|
return fmt.Errorf("unable to send presence to MUC %s: %w", room, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы не хотим ротировать, то цикл нам тут не нужен, просто выходим.
|
||||||
|
if j.C.Jabber.RuntimeStatus.RotationTime == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(totalSleepTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinMuc джойнится к конференциям/каналам/комнатам в джаббере.
|
||||||
|
func (j *Jabber) JoinMuc(room string) error {
|
||||||
|
log.Debugf("Sending disco#info from %s to %s", j.Talk.JID(), room)
|
||||||
|
|
||||||
|
if _, err := j.Talk.DiscoverInfo(j.Talk.JID(), room); err != nil {
|
||||||
|
return fmt.Errorf("unable to send disco#info to MUC %s: %w", room, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ждём, пока muc нам вернёт список фичей.
|
||||||
|
for i := 0; i < (20 * int(j.C.Jabber.ConnectionTimeout)); i++ {
|
||||||
|
var (
|
||||||
|
myRoom interface{}
|
||||||
|
supported bool
|
||||||
|
exist bool
|
||||||
|
)
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if myRoom, exist = j.MucCapsList.Get(room); !exist {
|
||||||
|
// Пока не задискаверилась.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist {
|
||||||
|
if supported {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Unable to join to password-protected room. Don't know how to enter passwords :)")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заходим в конфу, наконец-то
|
||||||
|
if _, err := j.Talk.JoinMUCNoHistory(room, j.GetBotNickFromRoomConfig(room)); err != nil {
|
||||||
|
return fmt.Errorf("unable to join to MUC: %s, %w", room, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Joining to MUC: %s", room)
|
||||||
|
|
||||||
|
// Ждём, когда прилетит presence из комнаты, тогда мы точно знаем, что мы вошли.
|
||||||
|
entered := false
|
||||||
|
|
||||||
|
for i := 0; i < (20 * int(j.C.Jabber.ConnectionTimeout)); i++ {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if slices.Contains(j.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(j.C.Jabber.ConnectionTimeout)+1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вот теперь точно можно слать статус.
|
||||||
|
log.Infof("Joined to MUC: %s", room)
|
||||||
|
|
||||||
|
j.GTomb.Go(func() error { return j.RotateStatus(room) })
|
||||||
|
|
||||||
|
// Время проверить участников на предмет злобности
|
||||||
|
namesInterface, present := j.RoomPresences.Get(room)
|
||||||
|
|
||||||
|
// Если room есть в списке presence-ов, то фигачим. room там должен быть, просто обязан.
|
||||||
|
if present {
|
||||||
|
for _, name := range InterfaceToStringSlice(namesInterface) {
|
||||||
|
var v xmpp.Presence
|
||||||
|
|
||||||
|
_ = json.Unmarshal([]byte(name), &v)
|
||||||
|
log.Infof("Fake presence forged for %s just for on-enter check", name)
|
||||||
|
// Оно там внутри всё само обработает, если вдруг возникнет wire error, то зарекконетится.
|
||||||
|
_ = j.BunyPresense(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EstablishConnection устанавливает соединение с jabber-сервером.
|
||||||
|
func (j *Jabber) EstablishConnection() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if j.Connecting && !j.IsConnected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проставляем глобальные переменные.
|
||||||
|
j.Connecting = true
|
||||||
|
j.IsConnected = false
|
||||||
|
j.RoomsConnected = make([]string, 0)
|
||||||
|
|
||||||
|
log.Debugf("Establishing connection to %s", j.Options.Host)
|
||||||
|
j.Talk, err = j.Options.NewClient()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to connect to %s: %w", j.Options.Host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
||||||
|
if _, err := j.Talk.SendKeepAlive(); err != nil {
|
||||||
|
return fmt.Errorf("try to send initial KeepAlive, got error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Connected")
|
||||||
|
|
||||||
|
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
|
||||||
|
for _, roomStruct := range j.C.Jabber.Channels {
|
||||||
|
room := roomStruct.Name
|
||||||
|
|
||||||
|
j.GTomb.Go(func() error { return j.JoinMuc(room) })
|
||||||
|
}
|
||||||
|
|
||||||
|
j.GTomb.Go(func() error { return j.RotateStatus("") })
|
||||||
|
|
||||||
|
j.LastActivity = time.Now().Unix()
|
||||||
|
j.Connecting = false
|
||||||
|
j.IsConnected = true
|
||||||
|
|
||||||
|
log.Debugf("Sending disco#info to %s", j.C.Jabber.Server)
|
||||||
|
|
||||||
|
_, err = j.Talk.DiscoverInfo(j.Talk.JID(), j.C.Jabber.Server)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to send disco#info to jabber server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProbeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
|
||||||
|
// они выкидывают (дисконнектят) клиента через некоторое время неактивности.
|
||||||
|
func (j *Jabber) ProbeServerLiveness() error { //nolint:gocognit
|
||||||
|
for {
|
||||||
|
for {
|
||||||
|
if j.Shutdown {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sleepTime := time.Duration(j.C.Jabber.ServerPingDelay) * 1000 * time.Millisecond
|
||||||
|
sleepTime += time.Duration(rand.Int63n(1000*j.C.Jabber.PingSplayDelay)) * time.Millisecond
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
|
if !j.IsConnected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay,
|
||||||
|
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay
|
||||||
|
// if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
if j.ServerCapsQueried { // Сервер ответил на disco#info
|
||||||
|
var (
|
||||||
|
value interface{}
|
||||||
|
exist bool
|
||||||
|
)
|
||||||
|
|
||||||
|
value, exist = j.ServerCapsList.Get("urn:xmpp:ping")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// Сервер анонсировал, что умеет в c2s пинги
|
||||||
|
case exist && value.(bool):
|
||||||
|
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
|
||||||
|
// максимальную корректировку разброса.
|
||||||
|
txTimeout := j.C.Jabber.ServerPingDelay + j.C.Jabber.ConnectionTimeout
|
||||||
|
txTimeout += j.C.Jabber.PingSplayDelay
|
||||||
|
rxTimeout := txTimeout
|
||||||
|
|
||||||
|
rxTimeAgo := time.Now().Unix() - j.ServerPingTimestampRx
|
||||||
|
|
||||||
|
if j.ServerPingTimestampTx > 0 { // Первая пуля от нас ушла...
|
||||||
|
switch {
|
||||||
|
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
|
||||||
|
case rxTimeAgo > (rxTimeout * 2):
|
||||||
|
err := fmt.Errorf( //nolint:goerr113
|
||||||
|
"stall connection detected. No c2s pong for %d seconds",
|
||||||
|
rxTimeAgo,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
|
||||||
|
// По-умолчанию, мы отправляем c2s пинг
|
||||||
|
default:
|
||||||
|
log.Debugf("Sending c2s ping from %s to %s", j.Talk.JID(), j.C.Jabber.Server)
|
||||||
|
|
||||||
|
if err := j.Talk.PingC2S(j.Talk.JID(), j.C.Jabber.Server); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ServerPingTimestampTx = time.Now().Unix()
|
||||||
|
}
|
||||||
|
} else { // Первая пуля пока не вылетела, отправляем
|
||||||
|
log.Debugf("Sending first c2s ping from %s to %s", j.Talk.JID(), j.C.Jabber.Server)
|
||||||
|
|
||||||
|
if err := j.Talk.PingC2S(j.Talk.JID(), j.C.Jabber.Server); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ServerPingTimestampTx = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сервер не анонсировал, что умеет в c2s пинги
|
||||||
|
default:
|
||||||
|
log.Debug("Sending keepalive whitespace ping")
|
||||||
|
|
||||||
|
if _, err := j.Talk.SendKeepAlive(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Сервер не ответил на disco#info
|
||||||
|
log.Debug("Sending keepalive whitespace ping")
|
||||||
|
|
||||||
|
if _, err := j.Talk.SendKeepAlive(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProbeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
||||||
|
func (j *Jabber) ProbeMUCLiveness() { //nolint:gocognit
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
for _, room := range j.RoomsConnected {
|
||||||
|
var (
|
||||||
|
exist bool
|
||||||
|
lastActivityTS interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||||
|
if lastActivityTS, exist = j.LastMucActivity.Get(room); !exist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если время последней активности в чятике не превысило
|
||||||
|
// j.C.Jabber.ServerPingDelay + j.C.Jabber.PingSplayDelay, ничего не пингуем.
|
||||||
|
if (time.Now().Unix() - lastActivityTS.(int64)) < (j.C.Jabber.ServerPingDelay + j.C.Jabber.PingSplayDelay) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Пинг MUC-а по сценарию без серверной оптимизации мы реализовывать не будем. Это как-то не надёжно.
|
||||||
|
j.GTomb.Go(
|
||||||
|
func() {
|
||||||
|
// Небольшая рандомная задержка перед пингом комнаты
|
||||||
|
sleepTime := time.Duration(rand.Int63n(1000*j.C.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
|
if e := j.Talk.PingS2S(talk.JID(), room+"/"+j.GetBotNickFromRoomConfig(room)); e != nil {
|
||||||
|
err = e
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
var roomMap interface{}
|
||||||
|
|
||||||
|
roomMap, exist = j.MucCapsList.Get(room)
|
||||||
|
|
||||||
|
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
|
||||||
|
// Server Optimization.
|
||||||
|
if exist && roomMap.(map[string]bool)["http://jabber.org/protocol/muc#self-ping-optimization"] {
|
||||||
|
j.GTomb.Go(
|
||||||
|
func() error {
|
||||||
|
// Небольшая рандомная задержка перед пингом комнаты.
|
||||||
|
sleepTime := time.Duration(rand.Int63n(1000*j.C.Jabber.PingSplayDelay)) * time.Millisecond
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
|
log.Debugf("Sending MUC ping from %s to %s", j.Talk.JID(), room)
|
||||||
|
|
||||||
|
if e := j.Talk.PingS2S(j.Talk.JID(), room); e != nil {
|
||||||
|
err = fmt.Errorf("unable to ping MUC %s: %w", room, e)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(j.C.Jabber.MucPingDelay) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
825
lib.go
825
lib.go
@@ -1,825 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/eleksir/go-xmpp"
|
|
||||||
"github.com/hjson/hjson-go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sigHandler Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
|
||||||
func sigHandler() {
|
|
||||||
log.Debug("Installing signal handler")
|
|
||||||
|
|
||||||
for s := range sigChan {
|
|
||||||
switch s {
|
|
||||||
case syscall.SIGINT:
|
|
||||||
log.Infoln("Got SIGINT, quitting")
|
|
||||||
case syscall.SIGTERM:
|
|
||||||
log.Infoln("Got SIGTERM, quitting")
|
|
||||||
case syscall.SIGQUIT:
|
|
||||||
log.Infoln("Got SIGQUIT, quitting")
|
|
||||||
|
|
||||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Чтобы не срать в логи ошибками, проставим 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
|
|
||||||
To: room,
|
|
||||||
Status: "Offline",
|
|
||||||
Type: "unavailable",
|
|
||||||
},
|
|
||||||
); err != nil {
|
|
||||||
log.Infof("Unable to send presence to jabber server: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// И закрываем соединение.
|
|
||||||
log.Debugf("Closing connection to jabber server")
|
|
||||||
|
|
||||||
if err := talk.Close(); err != nil {
|
|
||||||
log.Infof("Unable to close connection to jabber server: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readConfig читает и валидирует конфиг, а также выставляет некоторые default-ы, если значений для параметров в конфиге
|
|
||||||
// нет.
|
|
||||||
func readConfig() { //nolint:gocognit,gocyclo
|
|
||||||
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))
|
|
||||||
|
|
||||||
locations := []string{
|
|
||||||
"~/.buny-jabber-bot.json",
|
|
||||||
"~/buny-jabber-bot.json",
|
|
||||||
"/etc/buny-jabber-bot.json",
|
|
||||||
configJSONPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := os.ReadFile(location)
|
|
||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата.
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
|
||||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpJSON, &sampleConfig); err != nil {
|
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Значения для Jabber-клиента
|
|
||||||
if sampleConfig.Jabber.Server == "" {
|
|
||||||
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 = 5222
|
|
||||||
|
|
||||||
if sampleConfig.Jabber.Ssl {
|
|
||||||
if !sampleConfig.Jabber.StartTLS {
|
|
||||||
sampleConfig.Jabber.Port = 5223
|
|
||||||
|
|
||||||
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 {
|
|
||||||
sampleConfig.Jabber.StartTLS = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !sampleConfig.Jabber.Ssl || !sampleConfig.Jabber.StartTLS {
|
|
||||||
sampleConfig.Jabber.SslVerify = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// sampleConfig.Jabber.InsecureAllowUnencryptedAuth = false, если не задан
|
|
||||||
|
|
||||||
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 == "" {
|
|
||||||
log.Errorf("Jabber nick is not defined in config, quitting")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 не задан, то авторизации не будет
|
|
||||||
// Если sampleConfig.Jabber.Sasl не задан, то авторизация происходит через NickServ
|
|
||||||
|
|
||||||
// Если sampleConfig.Jabber.BanPhrasesEnable не задан, то он false
|
|
||||||
|
|
||||||
// Если список фраз, с которыми банят пустой, то вносим в него одну позицию с пустой строкой
|
|
||||||
if len(sampleConfig.Jabber.BanPhrases) == 0 {
|
|
||||||
sampleConfig.Jabber.BanPhrases[0] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если список фраз с которыми стартует бот пустой, вносим в него 1 запись с пустой строкой
|
|
||||||
if len(sampleConfig.Jabber.StartupStatus) == 0 {
|
|
||||||
sampleConfig.Jabber.StartupStatus[0] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если список статусов, с которыми работает бот пустой, вносим в него 1 запись с пустой строкой
|
|
||||||
if len(sampleConfig.Jabber.RuntimeStatus.Text) == 0 {
|
|
||||||
sampleConfig.Jabber.RuntimeStatus.Text[0] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если sampleConfig.Jabber.RuntimeStatus.RotationTime не задан, то он равен 0
|
|
||||||
// Если sampleConfig.Jabber.RuntimeStatus.RotationSplayTime не задан, то он равен 0
|
|
||||||
|
|
||||||
// Нам бот нужен в каких-то чат-румах, а не "просто так"
|
|
||||||
if len(sampleConfig.Jabber.Channels) < 1 {
|
|
||||||
log.Errorf("No jabber channels/rooms defined in config, quitting")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) //nolint:wsl
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !configLoaded {
|
|
||||||
log.Error("Config was not loaded! Refusing to start.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
|
|
||||||
locations := []string{
|
|
||||||
"~/.buny-jabber-bot-whitelist.json",
|
|
||||||
"~/buny-jabber-bot-whitelist.json",
|
|
||||||
"/etc/buny-jabber-bot-whitelist.json",
|
|
||||||
whitelistJSONPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, location := range locations {
|
|
||||||
fileInfo, err := os.Stat(location)
|
|
||||||
|
|
||||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Файл белого списка длинноват для белого списка, попробуем следующего кандидата
|
|
||||||
if fileInfo.Size() > 2097152 {
|
|
||||||
log.Warnf("Whitelist file %s is too long for whitelist, skipping", location)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := os.ReadFile(location)
|
|
||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip reading whitelist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
|
||||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
|
||||||
var (
|
|
||||||
sampleWhitelist myWhiteList
|
|
||||||
tmp map[string]interface{}
|
|
||||||
)
|
|
||||||
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
|
||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpJSON, err := json.Marshal(tmp)
|
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpJSON, &sampleWhitelist); err != nil {
|
|
||||||
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
whiteList = sampleWhitelist
|
|
||||||
whitelistLoaded = true
|
|
||||||
|
|
||||||
log.Infof("Using %s as whiteList file", location)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
locations := []string{
|
|
||||||
"~/.buny-jabber-bot-blacklist.json",
|
|
||||||
"~/buny-jabber-bot-blacklist.json",
|
|
||||||
"/etc/buny-jabber-bot-blacklist.json",
|
|
||||||
whitelistJSONPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, location := range locations {
|
|
||||||
fileInfo, err := os.Stat(location)
|
|
||||||
|
|
||||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Файл чёрного списка длинноват для чёрного списка, попробуем следующего кандидата
|
|
||||||
if fileInfo.Size() > 16777216 {
|
|
||||||
log.Warnf("Blacklist file %s is too long for blacklist, skipping", location)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf, err := os.ReadFile(location)
|
|
||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip reading blacklist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
|
||||||
// сериализуем в json, а потом json превращаем в структурку. Не очень эффективно, но он и нечасто требуется.
|
|
||||||
var (
|
|
||||||
sampleBlacklist myBlackList
|
|
||||||
tmp map[string]interface{}
|
|
||||||
)
|
|
||||||
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
|
||||||
|
|
||||||
// Не удалось распарсить - попробуем следующего кандидата
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing blacklist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpJSON, err := json.Marshal(tmp)
|
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("Skip parsing blacklist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpJSON, &sampleBlacklist); err != nil {
|
|
||||||
log.Warnf("Skip parsing whitelist file %s: %s", location, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
blackList = sampleBlacklist
|
|
||||||
blacklistLoaded = true
|
|
||||||
|
|
||||||
log.Infof("Using %s as blacklist file", location)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !blacklistLoaded {
|
|
||||||
log.Error("Blacklist was not loaded! Refusing to start.")
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// establishConnection устанавливает соединение с jabber-сервером.
|
|
||||||
func establishConnection() {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if connecting && !isConnected {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проставляем глобальные переменные.
|
|
||||||
connecting = true
|
|
||||||
isConnected = false
|
|
||||||
roomsConnected = make([]string, 0)
|
|
||||||
|
|
||||||
talk, err = options.NewClient()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// По идее keepalive должен же проходить только, если мы уже на сервере, так?
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
|
||||||
log.Errorf("Try to send initial KeepAlive, got error: %s", err)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Connected")
|
|
||||||
|
|
||||||
// Джойнимся к чятикам, но делаем это в фоне, чтобы не блочиться на ошибках, например, если бота забанили
|
|
||||||
for _, room := range config.Jabber.Channels {
|
|
||||||
go joinMuc(room)
|
|
||||||
}
|
|
||||||
|
|
||||||
go RotateStatus("")
|
|
||||||
|
|
||||||
lastActivity = time.Now().Unix()
|
|
||||||
connecting = false
|
|
||||||
isConnected = true
|
|
||||||
|
|
||||||
log.Debugf("Sending disco#info to %s", config.Jabber.Server)
|
|
||||||
|
|
||||||
_, err = talk.DiscoverInfo(talk.JID(), config.Jabber.Server)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("Unable to send disco#info to jabber server: %s", err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// joinMu джойнится к конференциям/каналам/комнатам в джаббере.
|
|
||||||
func joinMuc(room string) {
|
|
||||||
log.Debugf("Sending disco#info from %s to %s", talk.JID(), room)
|
|
||||||
|
|
||||||
if _, err := talk.DiscoverInfo(talk.JID(), room); err != nil {
|
|
||||||
log.Infof("Unable to send disco#info to MUC %s: %s", room, err)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждём, пока muc нам вернёт список фичей.
|
|
||||||
for i := 0; i < (20 * int(config.Jabber.ConnectionTimeout)); i++ {
|
|
||||||
var (
|
|
||||||
myRoom interface{}
|
|
||||||
supported bool
|
|
||||||
exist bool
|
|
||||||
)
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
if myRoom, exist = mucCapsList.Get(room); !exist {
|
|
||||||
// Пока не задискаверилась
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if supported, exist = myRoom.(map[string]bool)["muc_unsecured"]; exist {
|
|
||||||
if supported {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
go RotateStatus(room)
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeServerLiveness проверяет живость соединения с сервером. Для многих серверов обязательная штука, без которой
|
|
||||||
// они выкидывают клиента через некоторое время неактивности.
|
|
||||||
func probeServerLiveness() { //nolint:gocognit
|
|
||||||
defer gTomb.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-gTomb.Dying():
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
for {
|
|
||||||
if shutdown {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sleepTime := time.Duration(config.Jabber.ServerPingDelay) * 1000 * time.Millisecond
|
|
||||||
sleepTime += time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
|
||||||
time.Sleep(sleepTime)
|
|
||||||
|
|
||||||
if !isConnected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пингуем, только если не было никакой активности в течение > config.Jabber.ServerPingDelay,
|
|
||||||
// в худшем случе это будет ~ (config.Jabber.PingSplayDelay * 2) + config.Jabber.PingSplayDelay
|
|
||||||
// if (time.Now().Unix() - lastServerActivity) < (config.Jabber.ServerPingDelay + config.Jabber.PingSplayDelay) {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
if serverCapsQueried { // Сервер ответил на disco#info
|
|
||||||
var (
|
|
||||||
value interface{}
|
|
||||||
exist bool
|
|
||||||
)
|
|
||||||
|
|
||||||
value, exist = serverCapsList.Get("urn:xmpp:ping")
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// Сервер анонсировал, что умеет в c2s пинги
|
|
||||||
case exist && value.(bool):
|
|
||||||
// Таймаут c2s пинга. Возьмём сумму задержки между пингами, добавим таймаут коннекта и добавим
|
|
||||||
// максимальную корректировку разброса.
|
|
||||||
txTimeout := config.Jabber.ServerPingDelay + config.Jabber.ConnectionTimeout
|
|
||||||
txTimeout += config.Jabber.PingSplayDelay
|
|
||||||
rxTimeout := txTimeout
|
|
||||||
|
|
||||||
rxTimeAgo := time.Now().Unix() - serverPingTimestampRx
|
|
||||||
|
|
||||||
if serverPingTimestampTx > 0 { // Первая пуля от нас ушла...
|
|
||||||
switch {
|
|
||||||
// Давненько мы не получали понгов от сервера, вероятно, соединение с сервером утеряно?
|
|
||||||
case rxTimeAgo > (rxTimeout * 2):
|
|
||||||
err := fmt.Errorf( //nolint:goerr113
|
|
||||||
"stall connection detected. No c2s pong for %d seconds",
|
|
||||||
rxTimeAgo,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
// По-умолчанию, мы отправляем c2s пинг
|
|
||||||
default:
|
|
||||||
log.Debugf("Sending c2s ping from %s to %s", talk.JID(), config.Jabber.Server)
|
|
||||||
|
|
||||||
if err := talk.PingC2S(talk.JID(), config.Jabber.Server); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
serverPingTimestampTx = time.Now().Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сервер не анонсировал, что умеет в c2s пинги
|
|
||||||
default:
|
|
||||||
log.Debug("Sending keepalive whitespace ping")
|
|
||||||
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else { // Сервер не ответил на disco#info
|
|
||||||
log.Debug("Sending keepalive whitespace ping")
|
|
||||||
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
|
||||||
func probeMUCLiveness() { //nolint:gocognit
|
|
||||||
defer gTomb.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-gTomb.Dying():
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
for {
|
|
||||||
for _, room := range roomsConnected {
|
|
||||||
var (
|
|
||||||
exist bool
|
|
||||||
lastActivityTS interface{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Если записи про комнату нету, то пинговать её бессмысленно.
|
|
||||||
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) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Пинг MUC-а по сценарию без серверной оптимизации мы реализовывать не будем. Это как-то не надёжно.
|
|
||||||
go func(room string) {
|
|
||||||
// Небольшая рандомная задержка перед пингом комнаты
|
|
||||||
sleepTime := time.Duration(rand.Int63n(1000*config.Jabber.PingSplayDelay)) * time.Millisecond //nolint:gosec
|
|
||||||
time.Sleep(sleepTime)
|
|
||||||
|
|
||||||
if err := talk.PingS2S(talk.JID(), room+"/"+config.Jabber.Nick); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}(room)
|
|
||||||
*/
|
|
||||||
|
|
||||||
var roomMap interface{}
|
|
||||||
|
|
||||||
roomMap, exist = mucCapsList.Get(room)
|
|
||||||
|
|
||||||
// Пинги комнаты проводим, только если она записана, как прошедшая disco#info и поддерживающая
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
log.Debugf("Sending MUC ping from %s to %s", talk.JID(), room)
|
|
||||||
|
|
||||||
if err := talk.PingS2S(talk.JID(), room); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
}
|
|
||||||
}(room)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(config.Jabber.MucPingDelay) * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RotateStatus периодически изменяет статус бота в MUC-е согласно настройкам из кофига.
|
|
||||||
func RotateStatus(room string) {
|
|
||||||
defer gTomb.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-gTomb.Dying():
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
// TODO: Переделать на ticker-ы
|
|
||||||
totalSleepTime := time.Duration(config.Jabber.RuntimeStatus.RotationTime) * time.Second
|
|
||||||
totalSleepTime += time.Duration(config.Jabber.RuntimeStatus.RotationSplayTime) * time.Second
|
|
||||||
|
|
||||||
for {
|
|
||||||
status := randomPhrase(config.Jabber.RuntimeStatus.Text)
|
|
||||||
log.Debugf("Set status for MUC: %s to: %s", room, status)
|
|
||||||
|
|
||||||
var p xmpp.Presence
|
|
||||||
|
|
||||||
if room != "" {
|
|
||||||
p = xmpp.Presence{ //nolint:exhaustruct
|
|
||||||
To: room,
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p = xmpp.Presence{ //nolint:exhaustruct
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := talk.SendPresence(p); err != nil {
|
|
||||||
log.Infof("Unable to send presence to MUC %s: %s", room, err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если мы не хотим ротировать, то цикл нам тут не нужен, просто выходим.
|
|
||||||
if config.Jabber.RuntimeStatus.RotationTime == 0 {
|
|
||||||
gTomb.Done()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(totalSleepTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// randomPhrase Выдаёт одну рандомную фразу из даденного списка фраз.
|
|
||||||
func randomPhrase(list []string) string {
|
|
||||||
phrase := ""
|
|
||||||
|
|
||||||
if listLen := len(list); listLen > 0 {
|
|
||||||
phrase = list[rand.Intn(listLen)] //nolint:gosec
|
|
||||||
}
|
|
||||||
|
|
||||||
return phrase
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
199
main.go
199
main.go
@@ -1,199 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/eleksir/go-xmpp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"gopkg.in/tomb.v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFormatter(&log.TextFormatter{ //nolint:exhaustruct
|
|
||||||
DisableQuote: true,
|
|
||||||
DisableLevelTruncation: false,
|
|
||||||
DisableColors: true,
|
|
||||||
FullTimestamp: true,
|
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
|
||||||
})
|
|
||||||
|
|
||||||
readConfig()
|
|
||||||
|
|
||||||
// no panic
|
|
||||||
switch config.Loglevel {
|
|
||||||
case "fatal":
|
|
||||||
log.SetLevel(log.FatalLevel)
|
|
||||||
case "error":
|
|
||||||
log.SetLevel(log.ErrorLevel)
|
|
||||||
case "warn":
|
|
||||||
log.SetLevel(log.WarnLevel)
|
|
||||||
case "info":
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
case "debug":
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
case "trace":
|
|
||||||
log.SetLevel(log.TraceLevel)
|
|
||||||
default:
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Откроем лог и скормим его логгеру
|
|
||||||
if config.Log != "" {
|
|
||||||
logfile, err := os.OpenFile(config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Unable to open log file %s: %s", config.Log, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.SetOutput(logfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
myLogLevel := log.GetLevel()
|
|
||||||
log.Warnf("Loglevel set to %v", myLogLevel)
|
|
||||||
|
|
||||||
verboseClient := false
|
|
||||||
|
|
||||||
if myLogLevel == log.TraceLevel {
|
|
||||||
verboseClient = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Самое время вгрузить белые и чёрные списки, если что - получим ошибку в лог.
|
|
||||||
readWhitelist()
|
|
||||||
readBlacklist()
|
|
||||||
|
|
||||||
// github.com/mattn/go-xmpp пишет в stdio, нам этого не надо, ловим выхлоп его в logrus с уровнем trace
|
|
||||||
xmpp.DebugWriter = log.WithFields(log.Fields{"logger": "stdlib"}).WriterLevel(log.TraceLevel)
|
|
||||||
|
|
||||||
// Хэндлер сигналов не надо трогать, он нужен для завершения программы целиком
|
|
||||||
go sigHandler()
|
|
||||||
signal.Notify(sigChan, os.Interrupt)
|
|
||||||
|
|
||||||
for {
|
|
||||||
options = &xmpp.Options{ //nolint:exhaustruct
|
|
||||||
Host: fmt.Sprintf("%s:%d", config.Jabber.Server, config.Jabber.Port),
|
|
||||||
User: config.Jabber.User,
|
|
||||||
Password: config.Jabber.Password,
|
|
||||||
Resource: config.Jabber.Resource,
|
|
||||||
NoTLS: !config.Jabber.Ssl,
|
|
||||||
StartTLS: config.Jabber.StartTLS,
|
|
||||||
TLSConfig: &tls.Config{ //nolint:exhaustruct
|
|
||||||
ServerName: config.Jabber.Server,
|
|
||||||
InsecureSkipVerify: !config.Jabber.SslVerify, //nolint:gosec
|
|
||||||
},
|
|
||||||
InsecureAllowUnencryptedAuth: config.Jabber.InsecureAllowUnencryptedAuth,
|
|
||||||
Debug: verboseClient,
|
|
||||||
Session: false,
|
|
||||||
Status: "xa",
|
|
||||||
StatusMessage: randomPhrase(config.Jabber.StartupStatus),
|
|
||||||
DialTimeout: time.Duration(config.Jabber.ConnectionTimeout) * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Через tomb попробуем сделать выход горутинок управляемым
|
|
||||||
gTomb = tomb.Tomb{}
|
|
||||||
|
|
||||||
// Устанавливаем соединение и гребём события, посылаемые сервером
|
|
||||||
myLoop()
|
|
||||||
|
|
||||||
log.Error(gTomb.Wait())
|
|
||||||
time.Sleep(time.Duration(config.Jabber.ReconnectDelay) * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func myLoop() {
|
|
||||||
defer gTomb.Done()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-gTomb.Dying():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// Зададим начальное значение глобальным переменным
|
|
||||||
serverPingTimestampRx = 0
|
|
||||||
serverPingTimestampTx = 0
|
|
||||||
roomsConnected = make([]string, 1)
|
|
||||||
lastActivity = 0
|
|
||||||
lastServerActivity = 0
|
|
||||||
lastMucActivity = NewCollection()
|
|
||||||
serverCapsQueried = false
|
|
||||||
serverCapsList = NewCollection()
|
|
||||||
mucCapsList = NewCollection()
|
|
||||||
serverPingTimestampTx = 0
|
|
||||||
serverPingTimestampRx = 0
|
|
||||||
|
|
||||||
// Установим коннект
|
|
||||||
establishConnection()
|
|
||||||
|
|
||||||
serverPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен
|
|
||||||
|
|
||||||
// Тыкаем сервер палочкой, проверяем, что коннект жив и переустанавливаем его, если он не жив
|
|
||||||
go probeServerLiveness()
|
|
||||||
|
|
||||||
// Тыкаем muc-и палочкой, проверяем, что они живы и пере-заходим в них, если пинги пропали
|
|
||||||
go probeMUCLiveness()
|
|
||||||
|
|
||||||
// Гребём сообщения...
|
|
||||||
for {
|
|
||||||
// Стриггерилось завершение работы приложения, или соединение не установлено (порвалось, например)
|
|
||||||
// грести не надо
|
|
||||||
if shutdown {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isConnected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
chat, err := talk.Recv()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to get events from server: %s", err)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// Стрим не читается, он закрылся с той стороны во время чтения
|
|
||||||
case errors.Is(err, io.EOF):
|
|
||||||
err := fmt.Errorf("tcp stream closed while reading, err=%w", err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Пытаемся читать закрытый сокет
|
|
||||||
case errors.Is(err, net.ErrClosed):
|
|
||||||
err := fmt.Errorf("unable to read closed socket, err=%w", err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Не смогли записать в сокет
|
|
||||||
case errors.Is(err, net.ErrWriteToConnected):
|
|
||||||
err := fmt.Errorf("unable to write to socket, err=%w", err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
// Не сетевая проблема
|
|
||||||
default:
|
|
||||||
// Это уже что-то странное.
|
|
||||||
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
|
||||||
err := fmt.Errorf("error during parsing received message, err=%w", err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseEvent(chat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
110
types.go
110
types.go
@@ -1,110 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
// myConfig прототип структурки с конфигом.
|
|
||||||
type myConfig struct {
|
|
||||||
Jabber struct {
|
|
||||||
Server string `json:"server,omitempty"`
|
|
||||||
Port int `json:"port,omitempty"`
|
|
||||||
Ssl bool `json:"ssl,omitempty"`
|
|
||||||
StartTLS bool `json:"starttls,omitempty"`
|
|
||||||
SslVerify bool `json:"ssl_verify,omitempty"`
|
|
||||||
InsecureAllowUnencryptedAuth bool `json:"insecureallowunencryptedauth,omitempty"`
|
|
||||||
ConnectionTimeout int64 `json:"connection_timeout,omitempty"`
|
|
||||||
ReconnectDelay int64 `json:"reconnect_delay,omitempty"`
|
|
||||||
ServerPingDelay int64 `json:"server_ping_delay,omitempty"`
|
|
||||||
MucPingDelay int64 `json:"muc_ping_delay,omitempty"`
|
|
||||||
MucRejoinDelay int64 `json:"muc_rejoin_delay,omitempty"`
|
|
||||||
PingSplayDelay int64 `json:"ping_splay_delay,omitempty"`
|
|
||||||
Nick string `json:"nick,omitempty"`
|
|
||||||
Resource string `json:"resource,omitempty"`
|
|
||||||
User string `json:"user,omitempty"`
|
|
||||||
Password string `json:"password,omitempty"`
|
|
||||||
Channels []string `json:"channels"`
|
|
||||||
StartupStatus []string `json:"startup_status,omitempty"`
|
|
||||||
RuntimeStatus struct {
|
|
||||||
Text []string `json:"text,omitempty"`
|
|
||||||
RotationTime int64 `json:"rotation_time,omitempty"`
|
|
||||||
RotationSplayTime int64 `json:"rotation_splay_time,omitempty"`
|
|
||||||
} `json:"runtime_status,omitempty"`
|
|
||||||
BanPhrasesEnable bool `json:"ban_phrases_enable,omitempty"`
|
|
||||||
BanPhrases []string `json:"ban_phrases,omitempty"`
|
|
||||||
} `json:"jabber,omitempty"`
|
|
||||||
|
|
||||||
Loglevel string `json:"loglevel,omitempty"`
|
|
||||||
Log string `json:"log,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,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// myBlackList прототип структурки с чёрным списком jid-ов.
|
|
||||||
type myBlackList struct {
|
|
||||||
Blacklist []struct {
|
|
||||||
RoomName string `json:"room_name,omitempty"`
|
|
||||||
ReasonEnable bool `json:"reason_enable,omitempty"`
|
|
||||||
JidRe []string `json:"jid_re,omitempty"`
|
|
||||||
NickRe []string `json:"nick_re,omitempty"`
|
|
||||||
} `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"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
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"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
Items struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Node string `xml:"node,attr"`
|
|
||||||
MaxItems string `xml:"max_items,attr"`
|
|
||||||
} `xml:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// jabberTimeIqGetQuery прототип структурки для разбора IQ запросов на локальное время клиента,
|
|
||||||
// https://xmpp.org/extensions/xep-0202.html
|
|
||||||
type jabberTimeIqGetQuery struct {
|
|
||||||
// <time xmlns="urn:xmpp:time"/>
|
|
||||||
XMLName xml.Name `xml:"time"`
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
By string `xml:"by,attr"`
|
|
||||||
NotAcceptable struct {
|
|
||||||
Text string `xml:",chardata"`
|
|
||||||
Xmlns string `xml:"xmlns,attr"`
|
|
||||||
} `xml:"not-acceptable"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
66
vendor/github.com/eleksir/go-xmpp/.golangci.yml
generated
vendored
Normal file
66
vendor/github.com/eleksir/go-xmpp/.golangci.yml
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
skip-dirs:
|
||||||
|
- _example
|
||||||
|
modules-download-mode: readonly
|
||||||
|
allow-parallel-runners: true
|
||||||
|
go: '1.21'
|
||||||
|
output:
|
||||||
|
sort-results: true
|
||||||
|
linters-settings:
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
# Оно не используется для повышения безопасности или каких-то таких секъюрых вещей, оно используется для выборки
|
||||||
|
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
|
||||||
|
- G404 # Insecure random number source (rand)
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- contextcheck
|
||||||
|
- decorder
|
||||||
|
- durationcheck
|
||||||
|
- errchkjson
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- gci
|
||||||
|
- ginkgolinter
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
- gocritic
|
||||||
|
- godot
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- gosmopolitan
|
||||||
|
- grouper
|
||||||
|
- importas
|
||||||
|
- ireturn
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- nilnil
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nonamedreturns
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- reassign
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- stylecheck
|
||||||
|
- unconvert
|
||||||
|
- wastedassign
|
||||||
|
- whitespace
|
||||||
|
- wsl
|
||||||
10
vendor/github.com/eleksir/go-xmpp/README.md
generated
vendored
10
vendor/github.com/eleksir/go-xmpp/README.md
generated
vendored
@@ -3,12 +3,12 @@ go-xmpp
|
|||||||
|
|
||||||
go xmpp library.
|
go xmpp library.
|
||||||
|
|
||||||
[Original](https://github.com/rsc/rsc/blob/master/xmpp/xmpp.go) was done by [Russ Cox](https://github.com/rsc), then
|
[Original](https://github.com/rsc/rsc/blob/master/xmpp/xmpp.go) work done by [Russ Cox](https://github.com/rsc),
|
||||||
forked by [Yasuhiro Matsumoto](https://github.com/mattn).
|
then [Yasuhiro Matsumoto](https://github.com/mattn) fork it and continue to improve it.
|
||||||
|
|
||||||
Since both of them have amost zero interest in continuing develop of this lib, i decide to fork it.
|
Since both of them have almost zero interest to continue develop this lib, I decide to fork it.
|
||||||
|
|
||||||
Documentation can be found at [godoc project](https://godoc.org/github.com/eleksir/go-xmpp)
|
Documentation can be found at [godoc project](https://godoc.org/github.com/eleksir/go-xmpp)
|
||||||
|
|
||||||
This fork mainains my needs for xmpp protocol. Feel free to contribute via PR-s if you find it interesing too and feel
|
This fork maintains my needs for xmpp protocol. Feel free to contribute via PR-s if you find it interesting too and feel
|
||||||
that it needs more love than it already has.
|
that it needs more love than it already has. And of course fell free to for it if needed.
|
||||||
|
|||||||
672
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
672
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
File diff suppressed because it is too large
Load Diff
46
vendor/github.com/eleksir/go-xmpp/xmpp_avatar.go
generated
vendored
46
vendor/github.com/eleksir/go-xmpp/xmpp_avatar.go
generated
vendored
@@ -1,7 +1,7 @@
|
|||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1" //nolint:gosec // go fuck yourself, gosec. You don't even fucking read rfc. So shout the fuck up.
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data"
|
XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data" //nocritic:revive
|
||||||
XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata"
|
XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata" //nocritic:revive
|
||||||
)
|
)
|
||||||
|
|
||||||
type clientAvatarData struct {
|
type clientAvatarData struct {
|
||||||
@@ -52,8 +52,8 @@ type AvatarMetadata struct {
|
|||||||
|
|
||||||
func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
|
func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
|
||||||
var data clientAvatarData
|
var data clientAvatarData
|
||||||
err := xml.Unmarshal(itemsBody, &data)
|
|
||||||
if err != nil {
|
if err := xml.Unmarshal(itemsBody, &data); err != nil {
|
||||||
return AvatarData{}, err
|
return AvatarData{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,8 +64,9 @@ func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
|
|||||||
return AvatarData{}, err
|
return AvatarData{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := sha1.Sum(dataRaw)
|
hash := sha1.Sum(dataRaw) //nolint:gosec // go fuck yourself, gosec. This exact mechanics described in standard.
|
||||||
hashStr := hex.EncodeToString(hash[:])
|
hashStr := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
if hashStr != id {
|
if hashStr != id {
|
||||||
return AvatarData{}, errors.New("SHA1 hashes do not match")
|
return AvatarData{}, errors.New("SHA1 hashes do not match")
|
||||||
}
|
}
|
||||||
@@ -77,9 +78,12 @@ func handleAvatarData(itemsBody []byte, from, id string) (AvatarData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
|
func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
|
||||||
var meta clientAvatarMetadata
|
var (
|
||||||
err := xml.Unmarshal(body, &meta)
|
meta clientAvatarMetadata
|
||||||
if err != nil {
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = xml.Unmarshal(body, &meta); err != nil {
|
||||||
return AvatarMetadata{}, err
|
return AvatarMetadata{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +95,10 @@ func handleAvatarMetadata(body []byte, from string) (AvatarMetadata, error) {
|
|||||||
ID: meta.Info.ID,
|
ID: meta.Info.ID,
|
||||||
Type: meta.Info.Type,
|
Type: meta.Info.Type,
|
||||||
URL: meta.Info.URL,
|
URL: meta.Info.URL,
|
||||||
}, nil
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wrapper for atoi which just returns -1 if an error occurs
|
// atoiw is a wrapper for atoi which just returns -1 if an error occurs.
|
||||||
func atoiw(str string) int {
|
func atoiw(str string) int {
|
||||||
i, err := strconv.Atoi(str)
|
i, err := strconv.Atoi(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,22 +108,22 @@ func atoiw(str string) int {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AvatarSubscribeMetadata(jid string) {
|
func (c *Client) AvatarSubscribeMetadata(jid string) (string, error) {
|
||||||
c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
return c.PubsubSubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AvatarUnsubscribeMetadata(jid string) {
|
func (c *Client) AvatarUnsubscribeMetadata(jid string) (string, error) {
|
||||||
c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
return c.PubsubUnsubscribeNode(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AvatarRequestData(jid string) {
|
func (c *Client) AvatarRequestData(jid string) (string, error) {
|
||||||
c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid)
|
return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_DATA, jid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AvatarRequestDataByID(jid, id string) {
|
func (c *Client) AvatarRequestDataByID(jid, id string) (string, error) {
|
||||||
c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id)
|
return c.PubsubRequestItem(XMPPNS_AVATAR_PEP_DATA, jid, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AvatarRequestMetadata(jid string) {
|
func (c *Client) AvatarRequestMetadata(jid string) (string, error) {
|
||||||
c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid)
|
return c.PubsubRequestLastItems(XMPPNS_AVATAR_PEP_METADATA, jid)
|
||||||
}
|
}
|
||||||
|
|||||||
5
vendor/github.com/eleksir/go-xmpp/xmpp_disco.go
generated
vendored
5
vendor/github.com/eleksir/go-xmpp/xmpp_disco.go
generated
vendored
@@ -5,8 +5,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
|
XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items" //nocritic:revive
|
||||||
XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
|
XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info" //nocritic:revive
|
||||||
)
|
)
|
||||||
|
|
||||||
type clientDiscoFeature struct {
|
type clientDiscoFeature struct {
|
||||||
@@ -90,6 +90,7 @@ func clientIdentitiesToReturn(identities []clientDiscoIdentity) []DiscoIdentity
|
|||||||
|
|
||||||
func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem {
|
func clientDiscoItemsToReturn(items []clientDiscoItem) []DiscoItem {
|
||||||
var ret []DiscoItem
|
var ret []DiscoItem
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
ret = append(ret, DiscoItem{
|
ret = append(ret, DiscoItem{
|
||||||
Jid: item.Jid,
|
Jid: item.Jid,
|
||||||
|
|||||||
16
vendor/github.com/eleksir/go-xmpp/xmpp_information_query.go
generated
vendored
16
vendor/github.com/eleksir/go-xmpp/xmpp_information_query.go
generated
vendored
@@ -14,22 +14,25 @@ const IQTypeError = "error"
|
|||||||
func (c *Client) Discovery() (string, error) {
|
func (c *Client) Discovery() (string, error) {
|
||||||
// use getCookie for a pseudo random id.
|
// use getCookie for a pseudo random id.
|
||||||
reqID := strconv.FormatUint(uint64(getCookie()), 10)
|
reqID := strconv.FormatUint(uint64(getCookie()), 10)
|
||||||
|
|
||||||
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "")
|
return c.RawInformationQuery(c.jid, c.domain, reqID, IQTypeGet, XMPPNS_DISCO_ITEMS, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscoverNodeInfo discovers information about a node. Empty node queries info about server itself.
|
// DiscoverNodeInfo discovers information about a node. Empty node queries info about server itself.
|
||||||
func (c *Client) DiscoverNodeInfo(node string) (string, error) {
|
func (c *Client) DiscoverNodeInfo(node string) (string, error) {
|
||||||
query := fmt.Sprintf("<query xmlns='%s' node='%s'/>", XMPPNS_DISCO_INFO, node)
|
query := fmt.Sprintf("<query xmlns='%s' node='%s'/>", XMPPNS_DISCO_INFO, node)
|
||||||
|
|
||||||
return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query)
|
return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscoverInfo discovers information about given item from given jid.
|
// DiscoverInfo discovers information about given item from given jid.
|
||||||
func (c *Client) DiscoverInfo(from string, to string) (string, error) {
|
func (c *Client) DiscoverInfo(from string, to string) (string, error) {
|
||||||
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_INFO)
|
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_INFO)
|
||||||
|
|
||||||
return c.RawInformation(from, to, "info3", IQTypeGet, query)
|
return c.RawInformation(from, to, "info3", IQTypeGet, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiscoverServerItems discover items that the server exposes
|
// DiscoverServerItems discover items that the server exposes.
|
||||||
func (c *Client) DiscoverServerItems() (string, error) {
|
func (c *Client) DiscoverServerItems() (string, error) {
|
||||||
return c.DiscoverEntityItems(c.domain)
|
return c.DiscoverEntityItems(c.domain)
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,7 @@ func (c *Client) DiscoverServerItems() (string, error) {
|
|||||||
// DiscoverEntityItems discovers items that an entity exposes.
|
// DiscoverEntityItems discovers items that an entity exposes.
|
||||||
func (c *Client) DiscoverEntityItems(jid string) (string, error) {
|
func (c *Client) DiscoverEntityItems(jid string) (string, error) {
|
||||||
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_ITEMS)
|
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_ITEMS)
|
||||||
|
|
||||||
return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query)
|
return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ func (c *Client) DiscoverEntityItems(jid string) (string, error) {
|
|||||||
func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
|
func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, body string) (string, error) {
|
||||||
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'><query xmlns='%s'>%s</query></iq>"
|
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'><query xmlns='%s'>%s</query></iq>"
|
||||||
_, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
|
_, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
|
||||||
|
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +56,7 @@ func (c *Client) RawInformationQuery(from, to, id, iqType, requestNamespace, bod
|
|||||||
func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) {
|
func (c *Client) RawInformation(from, to, id, iqType, body string) (string, error) {
|
||||||
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'>%s</iq>"
|
const xmlIQ = "<iq from='%s' to='%s' id='%s' type='%s'>%s</iq>"
|
||||||
_, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
|
_, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
|
||||||
|
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +70,7 @@ func (c *Client) IqVersionResponse(v IQ, name, version, os string) (string, erro
|
|||||||
version = "undefined"
|
version = "undefined"
|
||||||
}
|
}
|
||||||
|
|
||||||
query := "<query xmlns=\"jabber:iq:version\">" //nolint:wsl
|
query := "<query xmlns=\"jabber:iq:version\">"
|
||||||
query += fmt.Sprintf("<name>%s</name>", name)
|
query += fmt.Sprintf("<name>%s</name>", name)
|
||||||
query += fmt.Sprintf("<version>%s</version>", version)
|
query += fmt.Sprintf("<version>%s</version>", version)
|
||||||
|
|
||||||
@@ -100,9 +106,9 @@ func (c *Client) JabberIqLastResponse(v IQ, lastActivity int64) (string, error)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UrnXmppTimeResponse implements response to query entity's current time (xep-0202).
|
// UrnXMPPTimeResponse implements response to query entity's current time (xep-0202).
|
||||||
// TimezoneOffset have format +HH:MM or -HH:MM
|
// TimezoneOffset have format +HH:MM or -HH:MM.
|
||||||
func (c *Client) UrnXmppTimeResponse(v IQ, timezoneOffset string) (string, error) {
|
func (c *Client) UrnXMPPTimeResponse(v IQ, timezoneOffset string) (string, error) {
|
||||||
query := fmt.Sprintf(
|
query := fmt.Sprintf(
|
||||||
"<time xmlns=\"urn:xmpp:time\"><tzo>%s</tzo><utc>%s</utc></time>",
|
"<time xmlns=\"urn:xmpp:time\"><tzo>%s</tzo><utc>%s</utc></time>",
|
||||||
timezoneOffset,
|
timezoneOffset,
|
||||||
|
|||||||
41
vendor/github.com/eleksir/go-xmpp/xmpp_muc.go
generated
vendored
41
vendor/github.com/eleksir/go-xmpp/xmpp_muc.go
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// TODO(flo):
|
// TODO(flo):
|
||||||
// - support password protected MUC rooms
|
|
||||||
// - cleanup signatures of join/leave functions
|
// - cleanup signatures of join/leave functions
|
||||||
package xmpp
|
package xmpp
|
||||||
|
|
||||||
@@ -23,16 +22,18 @@ const (
|
|||||||
SinceHistory = 4
|
SinceHistory = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send sends room topic wrapped inside an XMPP message stanza body.
|
// SendTopic sends room topic wrapped inside an XMPP message stanza body.
|
||||||
func (c *Client) SendTopic(chat Chat) (n int, err error) {
|
func (c *Client) SendTopic(chat Chat) (int, error) {
|
||||||
return fmt.Fprintf(StanzaWriter, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>",
|
return fmt.Fprintf(StanzaWriter, "<message to='%s' type='%s' xml:lang='en'>"+"<subject>%s</subject></message>",
|
||||||
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
|
xmlEscape(chat.Remote), xmlEscape(chat.Type), xmlEscape(chat.Text))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
|
// JoinMUCNoHistory joins room and instructs server that no history data required.
|
||||||
|
func (c *Client) JoinMUCNoHistory(jid, nick string) (int, error) {
|
||||||
if nick == "" {
|
if nick == "" {
|
||||||
nick = c.jid
|
nick = c.jid
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
||||||
"<x xmlns='%s'>"+
|
"<x xmlns='%s'>"+
|
||||||
"<history maxchars='0'/></x>\n"+
|
"<history maxchars='0'/></x>\n"+
|
||||||
@@ -40,12 +41,13 @@ func (c *Client) JoinMUCNoHistory(jid, nick string) (n int, err error) {
|
|||||||
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
||||||
}
|
}
|
||||||
|
|
||||||
// xep-0045 7.2
|
// JoinMUC joins room, xep-0045 7.2.
|
||||||
func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_date *time.Time) (n int, err error) {
|
func (c *Client) JoinMUC(jid, nick string, historyType, history int, historyDate *time.Time) (int, error) {
|
||||||
if nick == "" {
|
if nick == "" {
|
||||||
nick = c.jid
|
nick = c.jid
|
||||||
}
|
}
|
||||||
switch history_type {
|
|
||||||
|
switch historyType {
|
||||||
case NoHistory:
|
case NoHistory:
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
||||||
"<x xmlns='%s' />\n"+
|
"<x xmlns='%s' />\n"+
|
||||||
@@ -70,23 +72,25 @@ func (c *Client) JoinMUC(jid, nick string, history_type, history int, history_da
|
|||||||
"</presence>",
|
"</presence>",
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
||||||
case SinceHistory:
|
case SinceHistory:
|
||||||
if history_date != nil {
|
if historyDate != nil {
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
||||||
"<x xmlns='%s'>\n"+
|
"<x xmlns='%s'>\n"+
|
||||||
"<history since='%s'/></x>\n"+
|
"<history since='%s'/></x>\n"+
|
||||||
"</presence>",
|
"</presence>",
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history_date.Format(time.RFC3339))
|
xmlEscape(jid), xmlEscape(nick), nsMUC, historyDate.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0, errors.New("Unknown history option")
|
|
||||||
|
return 0, errors.New("unknown history option")
|
||||||
}
|
}
|
||||||
|
|
||||||
// xep-0045 7.2.6
|
// JoinProtectedMUC joins password protected room, xep-0045 7.2.6.
|
||||||
func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_type, history int, history_date *time.Time) (n int, err error) {
|
func (c *Client) JoinProtectedMUC(jid, nick string, password string, historyType, history int, historyDate *time.Time) (int, error) {
|
||||||
if nick == "" {
|
if nick == "" {
|
||||||
nick = c.jid
|
nick = c.jid
|
||||||
}
|
}
|
||||||
switch history_type {
|
|
||||||
|
switch historyType {
|
||||||
case NoHistory:
|
case NoHistory:
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
||||||
"<x xmlns='%s'>\n"+
|
"<x xmlns='%s'>\n"+
|
||||||
@@ -116,20 +120,21 @@ func (c *Client) JoinProtectedMUC(jid, nick string, password string, history_typ
|
|||||||
"</presence>",
|
"</presence>",
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
||||||
case SinceHistory:
|
case SinceHistory:
|
||||||
if history_date != nil {
|
if historyDate != nil {
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
||||||
"<x xmlns='%s'>\n"+
|
"<x xmlns='%s'>\n"+
|
||||||
"<password>%s</password>\n"+
|
"<password>%s</password>\n"+
|
||||||
"<history since='%s'/></x>\n"+
|
"<history since='%s'/></x>\n"+
|
||||||
"</presence>",
|
"</presence>",
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history_date.Format(time.RFC3339))
|
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), historyDate.Format(time.RFC3339))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0, errors.New("Unknown history option")
|
|
||||||
|
return 0, errors.New("unknown history option")
|
||||||
}
|
}
|
||||||
|
|
||||||
// xep-0045 7.14
|
// LeaveMUC quits from room xep-0045 7.14.
|
||||||
func (c *Client) LeaveMUC(jid string) (n int, err error) {
|
func (c *Client) LeaveMUC(jid string) (int, error) {
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence from='%s' to='%s' type='unavailable' />",
|
return fmt.Fprintf(StanzaWriter, "<presence from='%s' to='%s' type='unavailable' />",
|
||||||
c.jid, xmlEscape(jid))
|
c.jid, xmlEscape(jid))
|
||||||
}
|
}
|
||||||
|
|||||||
7
vendor/github.com/eleksir/go-xmpp/xmpp_ping.go
generated
vendored
7
vendor/github.com/eleksir/go-xmpp/xmpp_ping.go
generated
vendored
@@ -8,13 +8,16 @@ func (c *Client) PingC2S(jid, server string) error {
|
|||||||
if jid == "" {
|
if jid == "" {
|
||||||
jid = c.jid
|
jid = c.jid
|
||||||
}
|
}
|
||||||
|
|
||||||
if server == "" {
|
if server == "" {
|
||||||
server = c.domain
|
server = c.domain
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<iq from='%s' to='%s' id='c2s1' type='get'>\n"+
|
_, err := fmt.Fprintf(StanzaWriter, "<iq from='%s' to='%s' id='c2s1' type='get'>\n"+
|
||||||
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
||||||
"</iq>",
|
"</iq>",
|
||||||
xmlEscape(jid), xmlEscape(server))
|
xmlEscape(jid), xmlEscape(server))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,16 +26,18 @@ func (c *Client) PingS2S(fromServer, toServer string) error {
|
|||||||
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
||||||
"</iq>",
|
"</iq>",
|
||||||
xmlEscape(fromServer), xmlEscape(toServer))
|
xmlEscape(fromServer), xmlEscape(toServer))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SendResultPing(id, toServer string) error {
|
func (c *Client) SendResultPing(id, toServer string) error {
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<iq type='result' to='%s' id='%s'/>",
|
_, err := fmt.Fprintf(StanzaWriter, "<iq type='result' to='%s' id='%s'/>",
|
||||||
xmlEscape(toServer), xmlEscape(id))
|
xmlEscape(toServer), xmlEscape(id))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PingResponse responding to ping query according to xep-0199
|
// PingResponse responding to ping query according to xep-0199.
|
||||||
func (c *Client) PingResponse(v IQ) (string, error) {
|
func (c *Client) PingResponse(v IQ) (string, error) {
|
||||||
return c.RawInformation(
|
return c.RawInformation(
|
||||||
v.To,
|
v.To,
|
||||||
|
|||||||
10
vendor/github.com/eleksir/go-xmpp/xmpp_pubsub.go
generated
vendored
10
vendor/github.com/eleksir/go-xmpp/xmpp_pubsub.go
generated
vendored
@@ -6,8 +6,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub"
|
XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub" //nolint:revive
|
||||||
XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event"
|
XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event" //nolint:revive
|
||||||
)
|
)
|
||||||
|
|
||||||
type clientPubsubItem struct {
|
type clientPubsubItem struct {
|
||||||
@@ -67,7 +67,7 @@ type PubsubItems struct {
|
|||||||
Items []PubsubItem
|
Items []PubsubItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts []clientPubsubItem to []PubsubItem
|
// Converts []clientPubsubItem to []PubsubItem.
|
||||||
func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem {
|
func pubsubItemsToReturn(items []clientPubsubItem) []PubsubItem {
|
||||||
var tmp []PubsubItem
|
var tmp []PubsubItem
|
||||||
for _, i := range items {
|
for _, i := range items {
|
||||||
@@ -96,6 +96,7 @@ func pubsubSubscriptionStanza(node, jid string) string {
|
|||||||
body := fmt.Sprintf("<subscribe node='%s' jid='%s'/>",
|
body := fmt.Sprintf("<subscribe node='%s' jid='%s'/>",
|
||||||
xmlEscape(node),
|
xmlEscape(node),
|
||||||
xmlEscape(jid))
|
xmlEscape(jid))
|
||||||
|
|
||||||
return pubsubStanza(body)
|
return pubsubStanza(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ func pubsubUnsubscriptionStanza(node, jid string) string {
|
|||||||
body := fmt.Sprintf("<unsubscribe node='%s' jid='%s'/>",
|
body := fmt.Sprintf("<unsubscribe node='%s' jid='%s'/>",
|
||||||
xmlEscape(node),
|
xmlEscape(node),
|
||||||
xmlEscape(jid))
|
xmlEscape(jid))
|
||||||
|
|
||||||
return pubsubStanza(body)
|
return pubsubStanza(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,10 +126,12 @@ func (c *Client) PubsubUnsubscribeNode(node, jid string) (string, error) {
|
|||||||
|
|
||||||
func (c *Client) PubsubRequestLastItems(node, jid string) (string, error) {
|
func (c *Client) PubsubRequestLastItems(node, jid string) (string, error) {
|
||||||
body := fmt.Sprintf("<items node='%s'/>", node)
|
body := fmt.Sprintf("<items node='%s'/>", node)
|
||||||
|
|
||||||
return c.RawInformation(c.jid, jid, "items1", "get", pubsubStanza(body))
|
return c.RawInformation(c.jid, jid, "items1", "get", pubsubStanza(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) PubsubRequestItem(node, jid, id string) (string, error) {
|
func (c *Client) PubsubRequestItem(node, jid, id string) (string, error) {
|
||||||
body := fmt.Sprintf("<items node='%s'><item id='%s'/></items>", node, id)
|
body := fmt.Sprintf("<items node='%s'><item id='%s'/></items>", node, id)
|
||||||
|
|
||||||
return c.RawInformation(c.jid, jid, "items3", "get", pubsubStanza(body))
|
return c.RawInformation(c.jid, jid, "items3", "get", pubsubStanza(body))
|
||||||
}
|
}
|
||||||
|
|||||||
24
vendor/github.com/eleksir/go-xmpp/xmpp_subscription.go
generated
vendored
24
vendor/github.com/eleksir/go-xmpp/xmpp_subscription.go
generated
vendored
@@ -4,22 +4,30 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) ApproveSubscription(jid string) {
|
func (c *Client) ApproveSubscription(jid string) error {
|
||||||
fmt.Fprintf(StanzaWriter, "<presence to='%s' type='subscribed'/>",
|
_, err := fmt.Fprintf(StanzaWriter, "<presence to='%s' type='subscribed'/>",
|
||||||
xmlEscape(jid))
|
xmlEscape(jid))
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RevokeSubscription(jid string) {
|
func (c *Client) RevokeSubscription(jid string) error {
|
||||||
fmt.Fprintf(StanzaWriter, "<presence to='%s' type='unsubscribed'/>",
|
_, err := fmt.Fprintf(StanzaWriter, "<presence to='%s' type='unsubscribed'/>",
|
||||||
xmlEscape(jid))
|
xmlEscape(jid))
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RetrieveSubscription(jid string) {
|
func (c *Client) RetrieveSubscription(jid string) error {
|
||||||
fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribe'/>",
|
_, err := fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribe'/>",
|
||||||
xmlEscape(jid))
|
xmlEscape(jid))
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RequestSubscription(jid string) {
|
func (c *Client) RequestSubscription(jid string) error {
|
||||||
fmt.Fprintf(StanzaWriter, "<presence to='%s' type='subscribe'/>",
|
_, err := fmt.Fprintf(StanzaWriter, "<presence to='%s' type='subscribe'/>",
|
||||||
xmlEscape(jid))
|
xmlEscape(jid))
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
41
vendor/github.com/google/uuid/CHANGELOG.md
generated
vendored
Normal file
41
vendor/github.com/google/uuid/CHANGELOG.md
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06))
|
||||||
|
* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6))
|
||||||
|
|
||||||
|
## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29))
|
||||||
|
|
||||||
|
## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior)
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0))
|
||||||
|
|
||||||
|
## Changelog
|
||||||
26
vendor/github.com/google/uuid/CONTRIBUTING.md
generated
vendored
Normal file
26
vendor/github.com/google/uuid/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# How to contribute
|
||||||
|
|
||||||
|
We definitely welcome patches and contribution to this project!
|
||||||
|
|
||||||
|
### Tips
|
||||||
|
|
||||||
|
Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org).
|
||||||
|
|
||||||
|
Always try to include a test case! If it is not possible or not necessary,
|
||||||
|
please explain why in the pull request description.
|
||||||
|
|
||||||
|
### Releasing
|
||||||
|
|
||||||
|
Commits that would precipitate a SemVer change, as described in the Conventional
|
||||||
|
Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action)
|
||||||
|
to create a release candidate pull request. Once submitted, `release-please`
|
||||||
|
will create a release.
|
||||||
|
|
||||||
|
For tips on how to work with `release-please`, see its documentation.
|
||||||
|
|
||||||
|
### Legal requirements
|
||||||
|
|
||||||
|
In order to protect both you and ourselves, you will need to sign the
|
||||||
|
[Contributor License Agreement](https://cla.developers.google.com/clas).
|
||||||
|
|
||||||
|
You may have already signed it for other Google projects.
|
||||||
9
vendor/github.com/google/uuid/CONTRIBUTORS
generated
vendored
Normal file
9
vendor/github.com/google/uuid/CONTRIBUTORS
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Paul Borman <borman@google.com>
|
||||||
|
bmatsuo
|
||||||
|
shawnps
|
||||||
|
theory
|
||||||
|
jboverfelt
|
||||||
|
dsymonds
|
||||||
|
cd1
|
||||||
|
wallclockbuilder
|
||||||
|
dansouza
|
||||||
27
vendor/github.com/google/uuid/LICENSE
generated
vendored
Normal file
27
vendor/github.com/google/uuid/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2009,2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
21
vendor/github.com/google/uuid/README.md
generated
vendored
Normal file
21
vendor/github.com/google/uuid/README.md
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# uuid
|
||||||
|
The uuid package generates and inspects UUIDs based on
|
||||||
|
[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122)
|
||||||
|
and DCE 1.1: Authentication and Security Services.
|
||||||
|
|
||||||
|
This package is based on the github.com/pborman/uuid package (previously named
|
||||||
|
code.google.com/p/go-uuid). It differs from these earlier packages in that
|
||||||
|
a UUID is a 16 byte array rather than a byte slice. One loss due to this
|
||||||
|
change is the ability to represent an invalid UUID (vs a NIL UUID).
|
||||||
|
|
||||||
|
###### Install
|
||||||
|
```sh
|
||||||
|
go get github.com/google/uuid
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Documentation
|
||||||
|
[](https://pkg.go.dev/github.com/google/uuid)
|
||||||
|
|
||||||
|
Full `go doc` style documentation for the package can be viewed online without
|
||||||
|
installing this package by using the GoDoc site here:
|
||||||
|
http://pkg.go.dev/github.com/google/uuid
|
||||||
80
vendor/github.com/google/uuid/dce.go
generated
vendored
Normal file
80
vendor/github.com/google/uuid/dce.go
generated
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Domain represents a Version 2 domain
|
||||||
|
type Domain byte
|
||||||
|
|
||||||
|
// Domain constants for DCE Security (Version 2) UUIDs.
|
||||||
|
const (
|
||||||
|
Person = Domain(0)
|
||||||
|
Group = Domain(1)
|
||||||
|
Org = Domain(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDCESecurity returns a DCE Security (Version 2) UUID.
|
||||||
|
//
|
||||||
|
// The domain should be one of Person, Group or Org.
|
||||||
|
// On a POSIX system the id should be the users UID for the Person
|
||||||
|
// domain and the users GID for the Group. The meaning of id for
|
||||||
|
// the domain Org or on non-POSIX systems is site defined.
|
||||||
|
//
|
||||||
|
// For a given domain/id pair the same token may be returned for up to
|
||||||
|
// 7 minutes and 10 seconds.
|
||||||
|
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
|
||||||
|
uuid, err := NewUUID()
|
||||||
|
if err == nil {
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
|
||||||
|
uuid[9] = byte(domain)
|
||||||
|
binary.BigEndian.PutUint32(uuid[0:], id)
|
||||||
|
}
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
|
||||||
|
// domain with the id returned by os.Getuid.
|
||||||
|
//
|
||||||
|
// NewDCESecurity(Person, uint32(os.Getuid()))
|
||||||
|
func NewDCEPerson() (UUID, error) {
|
||||||
|
return NewDCESecurity(Person, uint32(os.Getuid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
|
||||||
|
// domain with the id returned by os.Getgid.
|
||||||
|
//
|
||||||
|
// NewDCESecurity(Group, uint32(os.Getgid()))
|
||||||
|
func NewDCEGroup() (UUID, error) {
|
||||||
|
return NewDCESecurity(Group, uint32(os.Getgid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain returns the domain for a Version 2 UUID. Domains are only defined
|
||||||
|
// for Version 2 UUIDs.
|
||||||
|
func (uuid UUID) Domain() Domain {
|
||||||
|
return Domain(uuid[9])
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
|
||||||
|
// UUIDs.
|
||||||
|
func (uuid UUID) ID() uint32 {
|
||||||
|
return binary.BigEndian.Uint32(uuid[0:4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Domain) String() string {
|
||||||
|
switch d {
|
||||||
|
case Person:
|
||||||
|
return "Person"
|
||||||
|
case Group:
|
||||||
|
return "Group"
|
||||||
|
case Org:
|
||||||
|
return "Org"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Domain%d", int(d))
|
||||||
|
}
|
||||||
12
vendor/github.com/google/uuid/doc.go
generated
vendored
Normal file
12
vendor/github.com/google/uuid/doc.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package uuid generates and inspects UUIDs.
|
||||||
|
//
|
||||||
|
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
|
||||||
|
// Services.
|
||||||
|
//
|
||||||
|
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
|
||||||
|
// maps or compared directly.
|
||||||
|
package uuid
|
||||||
59
vendor/github.com/google/uuid/hash.go
generated
vendored
Normal file
59
vendor/github.com/google/uuid/hash.go
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/sha1"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Well known namespace IDs and UUIDs
|
||||||
|
var (
|
||||||
|
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
|
||||||
|
Nil UUID // empty UUID, all zeros
|
||||||
|
|
||||||
|
// The Max UUID is special form of UUID that is specified to have all 128 bits set to 1.
|
||||||
|
Max = UUID{
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHash returns a new UUID derived from the hash of space concatenated with
|
||||||
|
// data generated by h. The hash should be at least 16 byte in length. The
|
||||||
|
// first 16 bytes of the hash are used to form the UUID. The version of the
|
||||||
|
// UUID will be the lower 4 bits of version. NewHash is used to implement
|
||||||
|
// NewMD5 and NewSHA1.
|
||||||
|
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
|
||||||
|
h.Reset()
|
||||||
|
h.Write(space[:]) //nolint:errcheck
|
||||||
|
h.Write(data) //nolint:errcheck
|
||||||
|
s := h.Sum(nil)
|
||||||
|
var uuid UUID
|
||||||
|
copy(uuid[:], s)
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
|
||||||
|
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMD5 returns a new MD5 (Version 3) UUID based on the
|
||||||
|
// supplied name space and data. It is the same as calling:
|
||||||
|
//
|
||||||
|
// NewHash(md5.New(), space, data, 3)
|
||||||
|
func NewMD5(space UUID, data []byte) UUID {
|
||||||
|
return NewHash(md5.New(), space, data, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
|
||||||
|
// supplied name space and data. It is the same as calling:
|
||||||
|
//
|
||||||
|
// NewHash(sha1.New(), space, data, 5)
|
||||||
|
func NewSHA1(space UUID, data []byte) UUID {
|
||||||
|
return NewHash(sha1.New(), space, data, 5)
|
||||||
|
}
|
||||||
38
vendor/github.com/google/uuid/marshal.go
generated
vendored
Normal file
38
vendor/github.com/google/uuid/marshal.go
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (uuid UUID) MarshalText() ([]byte, error) {
|
||||||
|
var js [36]byte
|
||||||
|
encodeHex(js[:], uuid)
|
||||||
|
return js[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (uuid *UUID) UnmarshalText(data []byte) error {
|
||||||
|
id, err := ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*uuid = id
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||||
|
func (uuid UUID) MarshalBinary() ([]byte, error) {
|
||||||
|
return uuid[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||||
|
func (uuid *UUID) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) != 16 {
|
||||||
|
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||||
|
}
|
||||||
|
copy(uuid[:], data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
90
vendor/github.com/google/uuid/node.go
generated
vendored
Normal file
90
vendor/github.com/google/uuid/node.go
generated
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
nodeMu sync.Mutex
|
||||||
|
ifname string // name of interface being used
|
||||||
|
nodeID [6]byte // hardware for version 1 UUIDs
|
||||||
|
zeroID [6]byte // nodeID with only 0's
|
||||||
|
)
|
||||||
|
|
||||||
|
// NodeInterface returns the name of the interface from which the NodeID was
|
||||||
|
// derived. The interface "user" is returned if the NodeID was set by
|
||||||
|
// SetNodeID.
|
||||||
|
func NodeInterface() string {
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
return ifname
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
|
||||||
|
// If name is "" then the first usable interface found will be used or a random
|
||||||
|
// Node ID will be generated. If a named interface cannot be found then false
|
||||||
|
// is returned.
|
||||||
|
//
|
||||||
|
// SetNodeInterface never fails when name is "".
|
||||||
|
func SetNodeInterface(name string) bool {
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
return setNodeInterface(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNodeInterface(name string) bool {
|
||||||
|
iname, addr := getHardwareInterface(name) // null implementation for js
|
||||||
|
if iname != "" && addr != nil {
|
||||||
|
ifname = iname
|
||||||
|
copy(nodeID[:], addr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found no interfaces with a valid hardware address. If name
|
||||||
|
// does not specify a specific interface generate a random Node ID
|
||||||
|
// (section 4.1.6)
|
||||||
|
if name == "" {
|
||||||
|
ifname = "random"
|
||||||
|
randomBits(nodeID[:])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
|
||||||
|
// if not already set.
|
||||||
|
func NodeID() []byte {
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
if nodeID == zeroID {
|
||||||
|
setNodeInterface("")
|
||||||
|
}
|
||||||
|
nid := nodeID
|
||||||
|
return nid[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
|
||||||
|
// of id are used. If id is less than 6 bytes then false is returned and the
|
||||||
|
// Node ID is not set.
|
||||||
|
func SetNodeID(id []byte) bool {
|
||||||
|
if len(id) < 6 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer nodeMu.Unlock()
|
||||||
|
nodeMu.Lock()
|
||||||
|
copy(nodeID[:], id)
|
||||||
|
ifname = "user"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
|
||||||
|
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
|
||||||
|
func (uuid UUID) NodeID() []byte {
|
||||||
|
var node [6]byte
|
||||||
|
copy(node[:], uuid[10:])
|
||||||
|
return node[:]
|
||||||
|
}
|
||||||
12
vendor/github.com/google/uuid/node_js.go
generated
vendored
Normal file
12
vendor/github.com/google/uuid/node_js.go
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright 2017 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build js
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
// getHardwareInterface returns nil values for the JS version of the code.
|
||||||
|
// This removes the "net" dependency, because it is not used in the browser.
|
||||||
|
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
|
||||||
|
func getHardwareInterface(name string) (string, []byte) { return "", nil }
|
||||||
33
vendor/github.com/google/uuid/node_net.go
generated
vendored
Normal file
33
vendor/github.com/google/uuid/node_net.go
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2017 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
var interfaces []net.Interface // cached list of interfaces
|
||||||
|
|
||||||
|
// getHardwareInterface returns the name and hardware address of interface name.
|
||||||
|
// If name is "" then the name and hardware address of one of the system's
|
||||||
|
// interfaces is returned. If no interfaces are found (name does not exist or
|
||||||
|
// there are no interfaces) then "", nil is returned.
|
||||||
|
//
|
||||||
|
// Only addresses of at least 6 bytes are returned.
|
||||||
|
func getHardwareInterface(name string) (string, []byte) {
|
||||||
|
if interfaces == nil {
|
||||||
|
var err error
|
||||||
|
interfaces, err = net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ifs := range interfaces {
|
||||||
|
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
|
||||||
|
return ifs.Name, ifs.HardwareAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
118
vendor/github.com/google/uuid/null.go
generated
vendored
Normal file
118
vendor/github.com/google/uuid/null.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// Copyright 2021 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jsonNull = []byte("null")
|
||||||
|
|
||||||
|
// NullUUID represents a UUID that may be null.
|
||||||
|
// NullUUID implements the SQL driver.Scanner interface so
|
||||||
|
// it can be used as a scan destination:
|
||||||
|
//
|
||||||
|
// var u uuid.NullUUID
|
||||||
|
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u)
|
||||||
|
// ...
|
||||||
|
// if u.Valid {
|
||||||
|
// // use u.UUID
|
||||||
|
// } else {
|
||||||
|
// // NULL value
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
type NullUUID struct {
|
||||||
|
UUID UUID
|
||||||
|
Valid bool // Valid is true if UUID is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the SQL driver.Scanner interface.
|
||||||
|
func (nu *NullUUID) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
nu.UUID, nu.Valid = Nil, false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := nu.UUID.Scan(value)
|
||||||
|
if err != nil {
|
||||||
|
nu.Valid = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nu.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (nu NullUUID) Value() (driver.Value, error) {
|
||||||
|
if !nu.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// Delegate to UUID Value function
|
||||||
|
return nu.UUID.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||||
|
func (nu NullUUID) MarshalBinary() ([]byte, error) {
|
||||||
|
if nu.Valid {
|
||||||
|
return nu.UUID[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||||
|
func (nu *NullUUID) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) != 16 {
|
||||||
|
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||||
|
}
|
||||||
|
copy(nu.UUID[:], data)
|
||||||
|
nu.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements encoding.TextMarshaler.
|
||||||
|
func (nu NullUUID) MarshalText() ([]byte, error) {
|
||||||
|
if nu.Valid {
|
||||||
|
return nu.UUID.MarshalText()
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonNull, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||||
|
func (nu *NullUUID) UnmarshalText(data []byte) error {
|
||||||
|
id, err := ParseBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
nu.Valid = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nu.UUID = id
|
||||||
|
nu.Valid = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
|
func (nu NullUUID) MarshalJSON() ([]byte, error) {
|
||||||
|
if nu.Valid {
|
||||||
|
return json.Marshal(nu.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonNull, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
|
func (nu *NullUUID) UnmarshalJSON(data []byte) error {
|
||||||
|
if bytes.Equal(data, jsonNull) {
|
||||||
|
*nu = NullUUID{}
|
||||||
|
return nil // valid null UUID
|
||||||
|
}
|
||||||
|
err := json.Unmarshal(data, &nu.UUID)
|
||||||
|
nu.Valid = err == nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
59
vendor/github.com/google/uuid/sql.go
generated
vendored
Normal file
59
vendor/github.com/google/uuid/sql.go
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scan implements sql.Scanner so UUIDs can be read from databases transparently.
|
||||||
|
// Currently, database types that map to string and []byte are supported. Please
|
||||||
|
// consult database-specific driver documentation for matching types.
|
||||||
|
func (uuid *UUID) Scan(src interface{}) error {
|
||||||
|
switch src := src.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case string:
|
||||||
|
// if an empty UUID comes from a table, we return a null UUID
|
||||||
|
if src == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// see Parse for required string format
|
||||||
|
u, err := Parse(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Scan: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*uuid = u
|
||||||
|
|
||||||
|
case []byte:
|
||||||
|
// if an empty UUID comes from a table, we return a null UUID
|
||||||
|
if len(src) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumes a simple slice of bytes if 16 bytes
|
||||||
|
// otherwise attempts to parse
|
||||||
|
if len(src) != 16 {
|
||||||
|
return uuid.Scan(string(src))
|
||||||
|
}
|
||||||
|
copy((*uuid)[:], src)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements sql.Valuer so that UUIDs can be written to databases
|
||||||
|
// transparently. Currently, UUIDs map to strings. Please consult
|
||||||
|
// database-specific driver documentation for matching types.
|
||||||
|
func (uuid UUID) Value() (driver.Value, error) {
|
||||||
|
return uuid.String(), nil
|
||||||
|
}
|
||||||
134
vendor/github.com/google/uuid/time.go
generated
vendored
Normal file
134
vendor/github.com/google/uuid/time.go
generated
vendored
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
|
||||||
|
// 1582.
|
||||||
|
type Time int64
|
||||||
|
|
||||||
|
const (
|
||||||
|
lillian = 2299160 // Julian day of 15 Oct 1582
|
||||||
|
unix = 2440587 // Julian day of 1 Jan 1970
|
||||||
|
epoch = unix - lillian // Days between epochs
|
||||||
|
g1582 = epoch * 86400 // seconds between epochs
|
||||||
|
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
timeMu sync.Mutex
|
||||||
|
lasttime uint64 // last time we returned
|
||||||
|
clockSeq uint16 // clock sequence for this run
|
||||||
|
|
||||||
|
timeNow = time.Now // for testing
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnixTime converts t the number of seconds and nanoseconds using the Unix
|
||||||
|
// epoch of 1 Jan 1970.
|
||||||
|
func (t Time) UnixTime() (sec, nsec int64) {
|
||||||
|
sec = int64(t - g1582ns100)
|
||||||
|
nsec = (sec % 10000000) * 100
|
||||||
|
sec /= 10000000
|
||||||
|
return sec, nsec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
|
||||||
|
// clock sequence as well as adjusting the clock sequence as needed. An error
|
||||||
|
// is returned if the current time cannot be determined.
|
||||||
|
func GetTime() (Time, uint16, error) {
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
timeMu.Lock()
|
||||||
|
return getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTime() (Time, uint16, error) {
|
||||||
|
t := timeNow()
|
||||||
|
|
||||||
|
// If we don't have a clock sequence already, set one.
|
||||||
|
if clockSeq == 0 {
|
||||||
|
setClockSequence(-1)
|
||||||
|
}
|
||||||
|
now := uint64(t.UnixNano()/100) + g1582ns100
|
||||||
|
|
||||||
|
// If time has gone backwards with this clock sequence then we
|
||||||
|
// increment the clock sequence
|
||||||
|
if now <= lasttime {
|
||||||
|
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
|
||||||
|
}
|
||||||
|
lasttime = now
|
||||||
|
return Time(now), clockSeq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClockSequence returns the current clock sequence, generating one if not
|
||||||
|
// already set. The clock sequence is only used for Version 1 UUIDs.
|
||||||
|
//
|
||||||
|
// The uuid package does not use global static storage for the clock sequence or
|
||||||
|
// the last time a UUID was generated. Unless SetClockSequence is used, a new
|
||||||
|
// random clock sequence is generated the first time a clock sequence is
|
||||||
|
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
|
||||||
|
func ClockSequence() int {
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
timeMu.Lock()
|
||||||
|
return clockSequence()
|
||||||
|
}
|
||||||
|
|
||||||
|
func clockSequence() int {
|
||||||
|
if clockSeq == 0 {
|
||||||
|
setClockSequence(-1)
|
||||||
|
}
|
||||||
|
return int(clockSeq & 0x3fff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
|
||||||
|
// -1 causes a new sequence to be generated.
|
||||||
|
func SetClockSequence(seq int) {
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
timeMu.Lock()
|
||||||
|
setClockSequence(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setClockSequence(seq int) {
|
||||||
|
if seq == -1 {
|
||||||
|
var b [2]byte
|
||||||
|
randomBits(b[:]) // clock sequence
|
||||||
|
seq = int(b[0])<<8 | int(b[1])
|
||||||
|
}
|
||||||
|
oldSeq := clockSeq
|
||||||
|
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
|
||||||
|
if oldSeq != clockSeq {
|
||||||
|
lasttime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
|
||||||
|
// uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs.
|
||||||
|
func (uuid UUID) Time() Time {
|
||||||
|
var t Time
|
||||||
|
switch uuid.Version() {
|
||||||
|
case 6:
|
||||||
|
time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110
|
||||||
|
t = Time(time)
|
||||||
|
case 7:
|
||||||
|
time := binary.BigEndian.Uint64(uuid[:8])
|
||||||
|
t = Time((time>>16)*10000 + g1582ns100)
|
||||||
|
default: // forward compatible
|
||||||
|
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
|
||||||
|
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
|
||||||
|
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
|
||||||
|
t = Time(time)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClockSequence returns the clock sequence encoded in uuid.
|
||||||
|
// The clock sequence is only well defined for version 1 and 2 UUIDs.
|
||||||
|
func (uuid UUID) ClockSequence() int {
|
||||||
|
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
|
||||||
|
}
|
||||||
43
vendor/github.com/google/uuid/util.go
generated
vendored
Normal file
43
vendor/github.com/google/uuid/util.go
generated
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// randomBits completely fills slice b with random data.
|
||||||
|
func randomBits(b []byte) {
|
||||||
|
if _, err := io.ReadFull(rander, b); err != nil {
|
||||||
|
panic(err.Error()) // rand should never fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||||
|
var xvalues = [256]byte{
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||||
|
}
|
||||||
|
|
||||||
|
// xtob converts hex characters x1 and x2 into a byte.
|
||||||
|
func xtob(x1, x2 byte) (byte, bool) {
|
||||||
|
b1 := xvalues[x1]
|
||||||
|
b2 := xvalues[x2]
|
||||||
|
return (b1 << 4) | b2, b1 != 255 && b2 != 255
|
||||||
|
}
|
||||||
365
vendor/github.com/google/uuid/uuid.go
generated
vendored
Normal file
365
vendor/github.com/google/uuid/uuid.go
generated
vendored
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
// Copyright 2018 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
|
||||||
|
// 4122.
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
// A Version represents a UUID's version.
|
||||||
|
type Version byte
|
||||||
|
|
||||||
|
// A Variant represents a UUID's variant.
|
||||||
|
type Variant byte
|
||||||
|
|
||||||
|
// Constants returned by Variant.
|
||||||
|
const (
|
||||||
|
Invalid = Variant(iota) // Invalid UUID
|
||||||
|
RFC4122 // The variant specified in RFC4122
|
||||||
|
Reserved // Reserved, NCS backward compatibility.
|
||||||
|
Microsoft // Reserved, Microsoft Corporation backward compatibility.
|
||||||
|
Future // Reserved for future definition.
|
||||||
|
)
|
||||||
|
|
||||||
|
const randPoolSize = 16 * 16
|
||||||
|
|
||||||
|
var (
|
||||||
|
rander = rand.Reader // random function
|
||||||
|
poolEnabled = false
|
||||||
|
poolMu sync.Mutex
|
||||||
|
poolPos = randPoolSize // protected with poolMu
|
||||||
|
pool [randPoolSize]byte // protected with poolMu
|
||||||
|
)
|
||||||
|
|
||||||
|
type invalidLengthError struct{ len int }
|
||||||
|
|
||||||
|
func (err invalidLengthError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid UUID length: %d", err.len)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInvalidLengthError is matcher function for custom error invalidLengthError
|
||||||
|
func IsInvalidLengthError(err error) bool {
|
||||||
|
_, ok := err.(invalidLengthError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse decodes s into a UUID or returns an error if it cannot be parsed. Both
|
||||||
|
// the standard UUID forms defined in RFC 4122
|
||||||
|
// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition,
|
||||||
|
// Parse accepts non-standard strings such as the raw hex encoding
|
||||||
|
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings,
|
||||||
|
// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are
|
||||||
|
// examined in the latter case. Parse should not be used to validate strings as
|
||||||
|
// it parses non-standard encodings as indicated above.
|
||||||
|
func Parse(s string) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
switch len(s) {
|
||||||
|
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
case 36:
|
||||||
|
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
case 36 + 9:
|
||||||
|
if !strings.EqualFold(s[:9], "urn:uuid:") {
|
||||||
|
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
|
||||||
|
}
|
||||||
|
s = s[9:]
|
||||||
|
|
||||||
|
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||||
|
case 36 + 2:
|
||||||
|
s = s[1:]
|
||||||
|
|
||||||
|
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
case 32:
|
||||||
|
var ok bool
|
||||||
|
for i := range uuid {
|
||||||
|
uuid[i], ok = xtob(s[i*2], s[i*2+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
default:
|
||||||
|
return uuid, invalidLengthError{len(s)}
|
||||||
|
}
|
||||||
|
// s is now at least 36 bytes long
|
||||||
|
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
for i, x := range [16]int{
|
||||||
|
0, 2, 4, 6,
|
||||||
|
9, 11,
|
||||||
|
14, 16,
|
||||||
|
19, 21,
|
||||||
|
24, 26, 28, 30, 32, 34,
|
||||||
|
} {
|
||||||
|
v, ok := xtob(s[x], s[x+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
uuid[i] = v
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
|
||||||
|
func ParseBytes(b []byte) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
switch len(b) {
|
||||||
|
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) {
|
||||||
|
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
|
||||||
|
}
|
||||||
|
b = b[9:]
|
||||||
|
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||||
|
b = b[1:]
|
||||||
|
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
var ok bool
|
||||||
|
for i := 0; i < 32; i += 2 {
|
||||||
|
uuid[i/2], ok = xtob(b[i], b[i+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
default:
|
||||||
|
return uuid, invalidLengthError{len(b)}
|
||||||
|
}
|
||||||
|
// s is now at least 36 bytes long
|
||||||
|
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
for i, x := range [16]int{
|
||||||
|
0, 2, 4, 6,
|
||||||
|
9, 11,
|
||||||
|
14, 16,
|
||||||
|
19, 21,
|
||||||
|
24, 26, 28, 30, 32, 34,
|
||||||
|
} {
|
||||||
|
v, ok := xtob(b[x], b[x+1])
|
||||||
|
if !ok {
|
||||||
|
return uuid, errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
uuid[i] = v
|
||||||
|
}
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParse is like Parse but panics if the string cannot be parsed.
|
||||||
|
// It simplifies safe initialization of global variables holding compiled UUIDs.
|
||||||
|
func MustParse(s string) UUID {
|
||||||
|
uuid, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(`uuid: Parse(` + s + `): ` + err.Error())
|
||||||
|
}
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
|
||||||
|
// does not have a length of 16. The bytes are copied from the slice.
|
||||||
|
func FromBytes(b []byte) (uuid UUID, err error) {
|
||||||
|
err = uuid.UnmarshalBinary(b)
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must returns uuid if err is nil and panics otherwise.
|
||||||
|
func Must(uuid UUID, err error) UUID {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate returns an error if s is not a properly formatted UUID in one of the following formats:
|
||||||
|
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||||
|
// It returns an error if the format is invalid, otherwise nil.
|
||||||
|
func Validate(s string) error {
|
||||||
|
switch len(s) {
|
||||||
|
// Standard UUID format
|
||||||
|
case 36:
|
||||||
|
|
||||||
|
// UUID with "urn:uuid:" prefix
|
||||||
|
case 36 + 9:
|
||||||
|
if !strings.EqualFold(s[:9], "urn:uuid:") {
|
||||||
|
return fmt.Errorf("invalid urn prefix: %q", s[:9])
|
||||||
|
}
|
||||||
|
s = s[9:]
|
||||||
|
|
||||||
|
// UUID enclosed in braces
|
||||||
|
case 36 + 2:
|
||||||
|
if s[0] != '{' || s[len(s)-1] != '}' {
|
||||||
|
return fmt.Errorf("invalid bracketed UUID format")
|
||||||
|
}
|
||||||
|
s = s[1 : len(s)-1]
|
||||||
|
|
||||||
|
// UUID without hyphens
|
||||||
|
case 32:
|
||||||
|
for i := 0; i < len(s); i += 2 {
|
||||||
|
_, ok := xtob(s[i], s[i+1])
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return invalidLengthError{len(s)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for standard UUID format
|
||||||
|
if len(s) == 36 {
|
||||||
|
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||||
|
return errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} {
|
||||||
|
if _, ok := xtob(s[x], s[x+1]); !ok {
|
||||||
|
return errors.New("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
// , or "" if uuid is invalid.
|
||||||
|
func (uuid UUID) String() string {
|
||||||
|
var buf [36]byte
|
||||||
|
encodeHex(buf[:], uuid)
|
||||||
|
return string(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// URN returns the RFC 2141 URN form of uuid,
|
||||||
|
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
|
||||||
|
func (uuid UUID) URN() string {
|
||||||
|
var buf [36 + 9]byte
|
||||||
|
copy(buf[:], "urn:uuid:")
|
||||||
|
encodeHex(buf[9:], uuid)
|
||||||
|
return string(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeHex(dst []byte, uuid UUID) {
|
||||||
|
hex.Encode(dst, uuid[:4])
|
||||||
|
dst[8] = '-'
|
||||||
|
hex.Encode(dst[9:13], uuid[4:6])
|
||||||
|
dst[13] = '-'
|
||||||
|
hex.Encode(dst[14:18], uuid[6:8])
|
||||||
|
dst[18] = '-'
|
||||||
|
hex.Encode(dst[19:23], uuid[8:10])
|
||||||
|
dst[23] = '-'
|
||||||
|
hex.Encode(dst[24:], uuid[10:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variant returns the variant encoded in uuid.
|
||||||
|
func (uuid UUID) Variant() Variant {
|
||||||
|
switch {
|
||||||
|
case (uuid[8] & 0xc0) == 0x80:
|
||||||
|
return RFC4122
|
||||||
|
case (uuid[8] & 0xe0) == 0xc0:
|
||||||
|
return Microsoft
|
||||||
|
case (uuid[8] & 0xe0) == 0xe0:
|
||||||
|
return Future
|
||||||
|
default:
|
||||||
|
return Reserved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version returns the version of uuid.
|
||||||
|
func (uuid UUID) Version() Version {
|
||||||
|
return Version(uuid[6] >> 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Version) String() string {
|
||||||
|
if v > 15 {
|
||||||
|
return fmt.Sprintf("BAD_VERSION_%d", v)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("VERSION_%d", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Variant) String() string {
|
||||||
|
switch v {
|
||||||
|
case RFC4122:
|
||||||
|
return "RFC4122"
|
||||||
|
case Reserved:
|
||||||
|
return "Reserved"
|
||||||
|
case Microsoft:
|
||||||
|
return "Microsoft"
|
||||||
|
case Future:
|
||||||
|
return "Future"
|
||||||
|
case Invalid:
|
||||||
|
return "Invalid"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("BadVariant%d", int(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRand sets the random number generator to r, which implements io.Reader.
|
||||||
|
// If r.Read returns an error when the package requests random data then
|
||||||
|
// a panic will be issued.
|
||||||
|
//
|
||||||
|
// Calling SetRand with nil sets the random number generator to the default
|
||||||
|
// generator.
|
||||||
|
func SetRand(r io.Reader) {
|
||||||
|
if r == nil {
|
||||||
|
rander = rand.Reader
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rander = r
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableRandPool enables internal randomness pool used for Random
|
||||||
|
// (Version 4) UUID generation. The pool contains random bytes read from
|
||||||
|
// the random number generator on demand in batches. Enabling the pool
|
||||||
|
// may improve the UUID generation throughput significantly.
|
||||||
|
//
|
||||||
|
// Since the pool is stored on the Go heap, this feature may be a bad fit
|
||||||
|
// for security sensitive applications.
|
||||||
|
//
|
||||||
|
// Both EnableRandPool and DisableRandPool are not thread-safe and should
|
||||||
|
// only be called when there is no possibility that New or any other
|
||||||
|
// UUID Version 4 generation function will be called concurrently.
|
||||||
|
func EnableRandPool() {
|
||||||
|
poolEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableRandPool disables the randomness pool if it was previously
|
||||||
|
// enabled with EnableRandPool.
|
||||||
|
//
|
||||||
|
// Both EnableRandPool and DisableRandPool are not thread-safe and should
|
||||||
|
// only be called when there is no possibility that New or any other
|
||||||
|
// UUID Version 4 generation function will be called concurrently.
|
||||||
|
func DisableRandPool() {
|
||||||
|
poolEnabled = false
|
||||||
|
defer poolMu.Unlock()
|
||||||
|
poolMu.Lock()
|
||||||
|
poolPos = randPoolSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// UUIDs is a slice of UUID types.
|
||||||
|
type UUIDs []UUID
|
||||||
|
|
||||||
|
// Strings returns a string slice containing the string form of each UUID in uuids.
|
||||||
|
func (uuids UUIDs) Strings() []string {
|
||||||
|
var uuidStrs = make([]string, len(uuids))
|
||||||
|
for i, uuid := range uuids {
|
||||||
|
uuidStrs[i] = uuid.String()
|
||||||
|
}
|
||||||
|
return uuidStrs
|
||||||
|
}
|
||||||
44
vendor/github.com/google/uuid/version1.go
generated
vendored
Normal file
44
vendor/github.com/google/uuid/version1.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
|
||||||
|
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||||
|
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||||
|
// be set NewUUID returns nil. If clock sequence has not been set by
|
||||||
|
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||||
|
// return the current NewUUID returns nil and an error.
|
||||||
|
//
|
||||||
|
// In most cases, New should be used.
|
||||||
|
func NewUUID() (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
now, seq, err := GetTime()
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeLow := uint32(now & 0xffffffff)
|
||||||
|
timeMid := uint16((now >> 32) & 0xffff)
|
||||||
|
timeHi := uint16((now >> 48) & 0x0fff)
|
||||||
|
timeHi |= 0x1000 // Version 1
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint32(uuid[0:], timeLow)
|
||||||
|
binary.BigEndian.PutUint16(uuid[4:], timeMid)
|
||||||
|
binary.BigEndian.PutUint16(uuid[6:], timeHi)
|
||||||
|
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||||
|
|
||||||
|
nodeMu.Lock()
|
||||||
|
if nodeID == zeroID {
|
||||||
|
setNodeInterface("")
|
||||||
|
}
|
||||||
|
copy(uuid[10:], nodeID[:])
|
||||||
|
nodeMu.Unlock()
|
||||||
|
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
76
vendor/github.com/google/uuid/version4.go
generated
vendored
Normal file
76
vendor/github.com/google/uuid/version4.go
generated
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright 2016 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// New creates a new random UUID or panics. New is equivalent to
|
||||||
|
// the expression
|
||||||
|
//
|
||||||
|
// uuid.Must(uuid.NewRandom())
|
||||||
|
func New() UUID {
|
||||||
|
return Must(NewRandom())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewString creates a new random UUID and returns it as a string or panics.
|
||||||
|
// NewString is equivalent to the expression
|
||||||
|
//
|
||||||
|
// uuid.New().String()
|
||||||
|
func NewString() string {
|
||||||
|
return Must(NewRandom()).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandom returns a Random (Version 4) UUID.
|
||||||
|
//
|
||||||
|
// The strength of the UUIDs is based on the strength of the crypto/rand
|
||||||
|
// package.
|
||||||
|
//
|
||||||
|
// Uses the randomness pool if it was enabled with EnableRandPool.
|
||||||
|
//
|
||||||
|
// A note about uniqueness derived from the UUID Wikipedia entry:
|
||||||
|
//
|
||||||
|
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
|
||||||
|
// hit by a meteorite is estimated to be one chance in 17 billion, that
|
||||||
|
// means the probability is about 0.00000000006 (6 × 10−11),
|
||||||
|
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||||
|
// year and having one duplicate.
|
||||||
|
func NewRandom() (UUID, error) {
|
||||||
|
if !poolEnabled {
|
||||||
|
return NewRandomFromReader(rander)
|
||||||
|
}
|
||||||
|
return newRandomFromPool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
|
||||||
|
func NewRandomFromReader(r io.Reader) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
_, err := io.ReadFull(r, uuid[:])
|
||||||
|
if err != nil {
|
||||||
|
return Nil, err
|
||||||
|
}
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||||
|
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRandomFromPool() (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
poolMu.Lock()
|
||||||
|
if poolPos == randPoolSize {
|
||||||
|
_, err := io.ReadFull(rander, pool[:])
|
||||||
|
if err != nil {
|
||||||
|
poolMu.Unlock()
|
||||||
|
return Nil, err
|
||||||
|
}
|
||||||
|
poolPos = 0
|
||||||
|
}
|
||||||
|
copy(uuid[:], pool[poolPos:(poolPos+16)])
|
||||||
|
poolPos += 16
|
||||||
|
poolMu.Unlock()
|
||||||
|
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||||
|
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
56
vendor/github.com/google/uuid/version6.go
generated
vendored
Normal file
56
vendor/github.com/google/uuid/version6.go
generated
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2023 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import "encoding/binary"
|
||||||
|
|
||||||
|
// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality.
|
||||||
|
// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs.
|
||||||
|
// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
|
||||||
|
//
|
||||||
|
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6
|
||||||
|
//
|
||||||
|
// NewV6 returns a Version 6 UUID based on the current NodeID and clock
|
||||||
|
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||||
|
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||||
|
// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by
|
||||||
|
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||||
|
// return the current NewV6 returns Nil and an error.
|
||||||
|
func NewV6() (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
now, seq, err := GetTime()
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| time_high |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| time_mid | time_low_and_version |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|clk_seq_hi_res | clk_seq_low | node (0-1) |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| node (2-5) |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
*/
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint64(uuid[0:], uint64(now))
|
||||||
|
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||||
|
|
||||||
|
uuid[6] = 0x60 | (uuid[6] & 0x0F)
|
||||||
|
uuid[8] = 0x80 | (uuid[8] & 0x3F)
|
||||||
|
|
||||||
|
nodeMu.Lock()
|
||||||
|
if nodeID == zeroID {
|
||||||
|
setNodeInterface("")
|
||||||
|
}
|
||||||
|
copy(uuid[10:], nodeID[:])
|
||||||
|
nodeMu.Unlock()
|
||||||
|
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
104
vendor/github.com/google/uuid/version7.go
generated
vendored
Normal file
104
vendor/github.com/google/uuid/version7.go
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright 2023 Google Inc. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UUID version 7 features a time-ordered value field derived from the widely
|
||||||
|
// implemented and well known Unix Epoch timestamp source,
|
||||||
|
// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded.
|
||||||
|
// As well as improved entropy characteristics over versions 1 or 6.
|
||||||
|
//
|
||||||
|
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7
|
||||||
|
//
|
||||||
|
// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
|
||||||
|
//
|
||||||
|
// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch).
|
||||||
|
// Uses the randomness pool if it was enabled with EnableRandPool.
|
||||||
|
// On error, NewV7 returns Nil and an error
|
||||||
|
func NewV7() (UUID, error) {
|
||||||
|
uuid, err := NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
makeV7(uuid[:])
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch).
|
||||||
|
// it use NewRandomFromReader fill random bits.
|
||||||
|
// On error, NewV7FromReader returns Nil and an error.
|
||||||
|
func NewV7FromReader(r io.Reader) (UUID, error) {
|
||||||
|
uuid, err := NewRandomFromReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
makeV7(uuid[:])
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
|
||||||
|
// uuid[8] already has the right version number (Variant is 10)
|
||||||
|
// see function NewV7 and NewV7FromReader
|
||||||
|
func makeV7(uuid []byte) {
|
||||||
|
/*
|
||||||
|
0 1 2 3
|
||||||
|
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| unix_ts_ms |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| unix_ts_ms | ver | rand_a (12 bit seq) |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
|var| rand_b |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
| rand_b |
|
||||||
|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||||
|
*/
|
||||||
|
_ = uuid[15] // bounds check
|
||||||
|
|
||||||
|
t, s := getV7Time()
|
||||||
|
|
||||||
|
uuid[0] = byte(t >> 40)
|
||||||
|
uuid[1] = byte(t >> 32)
|
||||||
|
uuid[2] = byte(t >> 24)
|
||||||
|
uuid[3] = byte(t >> 16)
|
||||||
|
uuid[4] = byte(t >> 8)
|
||||||
|
uuid[5] = byte(t)
|
||||||
|
|
||||||
|
uuid[6] = 0x70 | (0x0F & byte(s>>8))
|
||||||
|
uuid[7] = byte(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastV7time is the last time we returned stored as:
|
||||||
|
//
|
||||||
|
// 52 bits of time in milliseconds since epoch
|
||||||
|
// 12 bits of (fractional nanoseconds) >> 8
|
||||||
|
var lastV7time int64
|
||||||
|
|
||||||
|
const nanoPerMilli = 1000000
|
||||||
|
|
||||||
|
// getV7Time returns the time in milliseconds and nanoseconds / 256.
|
||||||
|
// The returned (milli << 12 + seq) is guarenteed to be greater than
|
||||||
|
// (milli << 12 + seq) returned by any previous call to getV7Time.
|
||||||
|
func getV7Time() (milli, seq int64) {
|
||||||
|
timeMu.Lock()
|
||||||
|
defer timeMu.Unlock()
|
||||||
|
|
||||||
|
nano := timeNow().UnixNano()
|
||||||
|
milli = nano / nanoPerMilli
|
||||||
|
// Sequence number is between 0 and 3906 (nanoPerMilli>>8)
|
||||||
|
seq = (nano - milli*nanoPerMilli) >> 8
|
||||||
|
now := milli<<12 + seq
|
||||||
|
if now <= lastV7time {
|
||||||
|
now = lastV7time + 1
|
||||||
|
milli = now >> 12
|
||||||
|
seq = now & 0xfff
|
||||||
|
}
|
||||||
|
lastV7time = now
|
||||||
|
return milli, seq
|
||||||
|
}
|
||||||
28
vendor/github.com/jbrukh/bayesian/LICENSE
generated
vendored
Normal file
28
vendor/github.com/jbrukh/bayesian/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Copyright (c) 2011. Jake Brukhman <jbrukh@gmail.com>
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions
|
||||||
|
are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above
|
||||||
|
copyright notice, this list of conditions and the
|
||||||
|
following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials
|
||||||
|
provided with the distribution.
|
||||||
|
3. The name of the author may not be used to endorse or
|
||||||
|
promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||||
|
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||||
|
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||||
|
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||||
|
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
126
vendor/github.com/jbrukh/bayesian/README.md
generated
vendored
Normal file
126
vendor/github.com/jbrukh/bayesian/README.md
generated
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Naive Bayesian Classification
|
||||||
|
|
||||||
|
Perform naive Bayesian classification into an arbitrary number of classes on sets of strings. `bayesian` also supports term frequency-inverse document frequency calculations ([TF-IDF](https://www.wikiwand.com/en/Tf%E2%80%93idf)).
|
||||||
|
|
||||||
|
Copyright (c) 2011-2017. Jake Brukhman. (jbrukh@gmail.com).
|
||||||
|
All rights reserved. See the LICENSE file for BSD-style license.
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
This is meant to be an low-entry barrier Go library for basic Bayesian classification. See code comments for a refresher on naive Bayesian classifiers, and please take some time to understand underflow edge cases as this otherwise may result in innacurate classifications.
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Using the go command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/jbrukh/bayesian
|
||||||
|
go install !$
|
||||||
|
```
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See the GoPkgDoc documentation [here](https://godoc.org/github.com/jbrukh/bayesian).
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Conditional probability and "log-likelihood"-like scoring.
|
||||||
|
- Underflow detection.
|
||||||
|
- Simple persistence of classifiers.
|
||||||
|
- Statistics.
|
||||||
|
- TF-IDF support.
|
||||||
|
|
||||||
|
------------
|
||||||
|
|
||||||
|
## Example 1 (Simple Classification)
|
||||||
|
|
||||||
|
To use the classifier, first you must create some classes
|
||||||
|
and train it:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/jbrukh/bayesian"
|
||||||
|
|
||||||
|
const (
|
||||||
|
Good bayesian.Class = "Good"
|
||||||
|
Bad bayesian.Class = "Bad"
|
||||||
|
)
|
||||||
|
|
||||||
|
classifier := bayesian.NewClassifier(Good, Bad)
|
||||||
|
goodStuff := []string{"tall", "rich", "handsome"}
|
||||||
|
badStuff := []string{"poor", "smelly", "ugly"}
|
||||||
|
classifier.Learn(goodStuff, Good)
|
||||||
|
classifier.Learn(badStuff, Bad)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can ascertain the scores of each class and
|
||||||
|
the most likely class your data belongs to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
scores, likely, _ := classifier.LogScores(
|
||||||
|
[]string{"tall", "girl"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Magnitude of the score indicates likelihood. Alternatively (but
|
||||||
|
with some risk of float underflow), you can obtain actual probabilities:
|
||||||
|
|
||||||
|
```go
|
||||||
|
probs, likely, _ := classifier.ProbScores(
|
||||||
|
[]string{"tall", "girl"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example 2 (TF-IDF Support)
|
||||||
|
|
||||||
|
To use the TF-IDF classifier, first you must create some classes
|
||||||
|
and train it and you need to call ConvertTermsFreqToTfIdf() AFTER training
|
||||||
|
and before calling classification methods such as `LogScores`, `SafeProbScores`, and `ProbScores`)
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/jbrukh/bayesian"
|
||||||
|
|
||||||
|
const (
|
||||||
|
Good bayesian.Class = "Good"
|
||||||
|
Bad bayesian.Class = "Bad"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a classifier with TF-IDF support.
|
||||||
|
classifier := bayesian.NewClassifierTfIdf(Good, Bad)
|
||||||
|
|
||||||
|
goodStuff := []string{"tall", "rich", "handsome"}
|
||||||
|
badStuff := []string{"poor", "smelly", "ugly"}
|
||||||
|
|
||||||
|
classifier.Learn(goodStuff, Good)
|
||||||
|
classifier.Learn(badStuff, Bad)
|
||||||
|
|
||||||
|
// Required
|
||||||
|
classifier.ConvertTermsFreqToTfIdf()
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can ascertain the scores of each class and
|
||||||
|
the most likely class your data belongs to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
scores, likely, _ := classifier.LogScores(
|
||||||
|
[]string{"tall", "girl"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Magnitude of the score indicates likelihood. Alternatively (but
|
||||||
|
with some risk of float underflow), you can obtain actual probabilities:
|
||||||
|
|
||||||
|
```go
|
||||||
|
probs, likely, _ := classifier.ProbScores(
|
||||||
|
[]string{"tall", "girl"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use wisely.
|
||||||
557
vendor/github.com/jbrukh/bayesian/bayesian.go
generated
vendored
Normal file
557
vendor/github.com/jbrukh/bayesian/bayesian.go
generated
vendored
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
package bayesian
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultProb is the tiny non-zero probability that a word
|
||||||
|
// we have not seen before appears in the class.
|
||||||
|
const defaultProb = 0.00000000001
|
||||||
|
|
||||||
|
// ErrUnderflow is returned when an underflow is detected.
|
||||||
|
var ErrUnderflow = errors.New("possible underflow detected")
|
||||||
|
|
||||||
|
// Class defines a class that the classifier will filter:
|
||||||
|
// C = {C_1, ..., C_n}. You should define your classes as a
|
||||||
|
// set of constants, for example as follows:
|
||||||
|
//
|
||||||
|
// const (
|
||||||
|
// Good Class = "Good"
|
||||||
|
// Bad Class = "Bad
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Class values should be unique.
|
||||||
|
type Class string
|
||||||
|
|
||||||
|
// Classifier implements the Naive Bayesian Classifier.
|
||||||
|
type Classifier struct {
|
||||||
|
Classes []Class
|
||||||
|
learned int // docs learned
|
||||||
|
seen int32 // docs seen
|
||||||
|
datas map[Class]*classData
|
||||||
|
tfIdf bool
|
||||||
|
DidConvertTfIdf bool // we can't classify a TF-IDF classifier if we haven't yet
|
||||||
|
// called ConverTermsFreqToTfIdf
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializableClassifier represents a container for
|
||||||
|
// Classifier objects whose fields are modifiable by
|
||||||
|
// reflection and are therefore writeable by gob.
|
||||||
|
type serializableClassifier struct {
|
||||||
|
Classes []Class
|
||||||
|
Learned int
|
||||||
|
Seen int
|
||||||
|
Datas map[Class]*classData
|
||||||
|
TfIdf bool
|
||||||
|
DidConvertTfIdf bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// classData holds the frequency data for words in a
|
||||||
|
// particular class. In the future, we may replace this
|
||||||
|
// structure with a trie-like structure for more
|
||||||
|
// efficient storage.
|
||||||
|
type classData struct {
|
||||||
|
Freqs map[string]float64
|
||||||
|
FreqTfs map[string][]float64
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
// newClassData creates a new empty classData node.
|
||||||
|
func newClassData() *classData {
|
||||||
|
return &classData{
|
||||||
|
Freqs: make(map[string]float64),
|
||||||
|
FreqTfs: make(map[string][]float64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWordProb returns P(W|C_j) -- the probability of seeing
|
||||||
|
// a particular word W in a document of this class.
|
||||||
|
func (d *classData) getWordProb(word string) float64 {
|
||||||
|
value, ok := d.Freqs[word]
|
||||||
|
if !ok {
|
||||||
|
return defaultProb
|
||||||
|
}
|
||||||
|
return float64(value) / float64(d.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWordsProb returns P(D|C_j) -- the probability of seeing
|
||||||
|
// this set of words in a document of this class.
|
||||||
|
//
|
||||||
|
// Note that words should not be empty, and this method of
|
||||||
|
// calulation is prone to underflow if there are many words
|
||||||
|
// and their individual probabilties are small.
|
||||||
|
func (d *classData) getWordsProb(words []string) (prob float64) {
|
||||||
|
prob = 1
|
||||||
|
for _, word := range words {
|
||||||
|
prob *= d.getWordProb(word)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClassifierTfIdf returns a new classifier. The classes provided
|
||||||
|
// should be at least 2 in number and unique, or this method will
|
||||||
|
// panic.
|
||||||
|
func NewClassifierTfIdf(classes ...Class) (c *Classifier) {
|
||||||
|
n := len(classes)
|
||||||
|
|
||||||
|
// check size
|
||||||
|
if n < 2 {
|
||||||
|
panic("provide at least two classes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check uniqueness
|
||||||
|
check := make(map[Class]bool, n)
|
||||||
|
for _, class := range classes {
|
||||||
|
check[class] = true
|
||||||
|
}
|
||||||
|
if len(check) != n {
|
||||||
|
panic("classes must be unique")
|
||||||
|
}
|
||||||
|
// create the classifier
|
||||||
|
c = &Classifier{
|
||||||
|
Classes: classes,
|
||||||
|
datas: make(map[Class]*classData, n),
|
||||||
|
tfIdf: true,
|
||||||
|
}
|
||||||
|
for _, class := range classes {
|
||||||
|
c.datas[class] = newClassData()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClassifier returns a new classifier. The classes provided
|
||||||
|
// should be at least 2 in number and unique, or this method will
|
||||||
|
// panic.
|
||||||
|
func NewClassifier(classes ...Class) (c *Classifier) {
|
||||||
|
n := len(classes)
|
||||||
|
|
||||||
|
// check size
|
||||||
|
if n < 2 {
|
||||||
|
panic("provide at least two classes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check uniqueness
|
||||||
|
check := make(map[Class]bool, n)
|
||||||
|
for _, class := range classes {
|
||||||
|
check[class] = true
|
||||||
|
}
|
||||||
|
if len(check) != n {
|
||||||
|
panic("classes must be unique")
|
||||||
|
}
|
||||||
|
// create the classifier
|
||||||
|
c = &Classifier{
|
||||||
|
Classes: classes,
|
||||||
|
datas: make(map[Class]*classData, n),
|
||||||
|
tfIdf: false,
|
||||||
|
DidConvertTfIdf: false,
|
||||||
|
}
|
||||||
|
for _, class := range classes {
|
||||||
|
c.datas[class] = newClassData()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClassifierFromFile loads an existing classifier from
|
||||||
|
// file. The classifier was previously saved with a call
|
||||||
|
// to c.WriteToFile(string).
|
||||||
|
func NewClassifierFromFile(name string) (c *Classifier, err error) {
|
||||||
|
file, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return NewClassifierFromReader(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClassifierFromReader: This actually does the deserializing of a Gob encoded classifier
|
||||||
|
func NewClassifierFromReader(r io.Reader) (c *Classifier, err error) {
|
||||||
|
dec := gob.NewDecoder(r)
|
||||||
|
w := new(serializableClassifier)
|
||||||
|
err = dec.Decode(w)
|
||||||
|
|
||||||
|
return &Classifier{w.Classes, w.Learned, int32(w.Seen), w.Datas, w.TfIdf, w.DidConvertTfIdf}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPriors returns the prior probabilities for the
|
||||||
|
// classes provided -- P(C_j).
|
||||||
|
//
|
||||||
|
// TODO: There is a way to smooth priors, currently
|
||||||
|
// not implemented here.
|
||||||
|
func (c *Classifier) getPriors() (priors []float64) {
|
||||||
|
n := len(c.Classes)
|
||||||
|
priors = make([]float64, n, n)
|
||||||
|
sum := 0
|
||||||
|
for index, class := range c.Classes {
|
||||||
|
total := c.datas[class].Total
|
||||||
|
priors[index] = float64(total)
|
||||||
|
sum += total
|
||||||
|
}
|
||||||
|
if sum != 0 {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
priors[i] /= float64(sum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Learned returns the number of documents ever learned
|
||||||
|
// in the lifetime of this classifier.
|
||||||
|
func (c *Classifier) Learned() int {
|
||||||
|
return c.learned
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seen returns the number of documents ever classified
|
||||||
|
// in the lifetime of this classifier.
|
||||||
|
func (c *Classifier) Seen() int {
|
||||||
|
return int(atomic.LoadInt32(&c.seen))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// IsTfIdf returns true if we are a classifier of type TfIdf
|
||||||
|
func (c *Classifier) IsTfIdf() bool {
|
||||||
|
return c.tfIdf
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordCount returns the number of words counted for
|
||||||
|
// each class in the lifetime of the classifier.
|
||||||
|
func (c *Classifier) WordCount() (result []int) {
|
||||||
|
result = make([]int, len(c.Classes))
|
||||||
|
for inx, class := range c.Classes {
|
||||||
|
data := c.datas[class]
|
||||||
|
result[inx] = data.Total
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe should be used when word-frequencies have been already been learned
|
||||||
|
// externally (e.g., hadoop)
|
||||||
|
func (c *Classifier) Observe(word string, count int, which Class) {
|
||||||
|
data := c.datas[which]
|
||||||
|
data.Freqs[word] += float64(count)
|
||||||
|
data.Total += count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Learn will accept new training documents for
|
||||||
|
// supervised learning.
|
||||||
|
func (c *Classifier) Learn(document []string, which Class) {
|
||||||
|
|
||||||
|
// If we are a tfidf classifier we first need to get terms as
|
||||||
|
// terms frequency and store that to work out the idf part later
|
||||||
|
// in ConvertToIDF().
|
||||||
|
if c.tfIdf {
|
||||||
|
if c.DidConvertTfIdf {
|
||||||
|
panic("Cannot call ConvertTermsFreqToTfIdf more than once. Reset and relearn to reconvert.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Term Frequency: word count in document / document length
|
||||||
|
docTf := make(map[string]float64)
|
||||||
|
for _, word := range document {
|
||||||
|
docTf[word]++
|
||||||
|
}
|
||||||
|
|
||||||
|
docLen := float64(len(document))
|
||||||
|
|
||||||
|
for wIndex, wCount := range docTf {
|
||||||
|
docTf[wIndex] = wCount / docLen
|
||||||
|
// add the TF sample, after training we can get IDF values.
|
||||||
|
c.datas[which].FreqTfs[wIndex] = append(c.datas[which].FreqTfs[wIndex], docTf[wIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data := c.datas[which]
|
||||||
|
for _, word := range document {
|
||||||
|
data.Freqs[word]++
|
||||||
|
data.Total++
|
||||||
|
}
|
||||||
|
c.learned++
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertTermsFreqToTfIdf uses all the TF samples for the class and converts
|
||||||
|
// them to TF-IDF https://en.wikipedia.org/wiki/Tf%E2%80%93idf
|
||||||
|
// once we have finished learning all the classes and have the totals.
|
||||||
|
func (c *Classifier) ConvertTermsFreqToTfIdf() {
|
||||||
|
|
||||||
|
if c.DidConvertTfIdf {
|
||||||
|
panic("Cannot call ConvertTermsFreqToTfIdf more than once. Reset and relearn to reconvert.")
|
||||||
|
}
|
||||||
|
|
||||||
|
for className := range c.datas {
|
||||||
|
|
||||||
|
for wIndex := range c.datas[className].FreqTfs {
|
||||||
|
tfIdfAdder := float64(0)
|
||||||
|
|
||||||
|
for tfSampleIndex := range c.datas[className].FreqTfs[wIndex] {
|
||||||
|
|
||||||
|
// we always want a possitive TF-IDF score.
|
||||||
|
tf := c.datas[className].FreqTfs[wIndex][tfSampleIndex]
|
||||||
|
c.datas[className].FreqTfs[wIndex][tfSampleIndex] = math.Log1p(tf) * math.Log1p(float64(c.learned)/float64(c.datas[className].Total))
|
||||||
|
tfIdfAdder += c.datas[className].FreqTfs[wIndex][tfSampleIndex]
|
||||||
|
}
|
||||||
|
// convert the 'counts' to TF-IDF's
|
||||||
|
c.datas[className].Freqs[wIndex] = tfIdfAdder
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanity check
|
||||||
|
c.DidConvertTfIdf = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogScores produces "log-likelihood"-like scores that can
|
||||||
|
// be used to classify documents into classes.
|
||||||
|
//
|
||||||
|
// The value of the score is proportional to the likelihood,
|
||||||
|
// as determined by the classifier, that the given document
|
||||||
|
// belongs to the given class. This is true even when scores
|
||||||
|
// returned are negative, which they will be (since we are
|
||||||
|
// taking logs of probabilities).
|
||||||
|
//
|
||||||
|
// The index j of the score corresponds to the class given
|
||||||
|
// by c.Classes[j].
|
||||||
|
//
|
||||||
|
// Additionally returned are "inx" and "strict" values. The
|
||||||
|
// inx corresponds to the maximum score in the array. If more
|
||||||
|
// than one of the scores holds the maximum values, then
|
||||||
|
// strict is false.
|
||||||
|
//
|
||||||
|
// Unlike c.Probabilities(), this function is not prone to
|
||||||
|
// floating point underflow and is relatively safe to use.
|
||||||
|
func (c *Classifier) LogScores(document []string) (scores []float64, inx int, strict bool) {
|
||||||
|
if c.tfIdf && !c.DidConvertTfIdf {
|
||||||
|
panic("Using a TF-IDF classifier. Please call ConvertTermsFreqToTfIdf before calling LogScores.")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(c.Classes)
|
||||||
|
scores = make([]float64, n, n)
|
||||||
|
priors := c.getPriors()
|
||||||
|
|
||||||
|
// calculate the score for each class
|
||||||
|
for index, class := range c.Classes {
|
||||||
|
data := c.datas[class]
|
||||||
|
// c is the sum of the logarithms
|
||||||
|
// as outlined in the refresher
|
||||||
|
score := math.Log(priors[index])
|
||||||
|
for _, word := range document {
|
||||||
|
score += math.Log(data.getWordProb(word))
|
||||||
|
}
|
||||||
|
scores[index] = score
|
||||||
|
}
|
||||||
|
inx, strict = findMax(scores)
|
||||||
|
atomic.AddInt32(&c.seen, 1)
|
||||||
|
return scores, inx, strict
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProbScores works the same as LogScores, but delivers
|
||||||
|
// actual probabilities as discussed above. Note that float64
|
||||||
|
// underflow is possible if the word list contains too
|
||||||
|
// many words that have probabilities very close to 0.
|
||||||
|
//
|
||||||
|
// Notes on underflow: underflow is going to occur when you're
|
||||||
|
// trying to assess large numbers of words that you have
|
||||||
|
// never seen before. Depending on the application, this
|
||||||
|
// may or may not be a concern. Consider using SafeProbScores()
|
||||||
|
// instead.
|
||||||
|
func (c *Classifier) ProbScores(doc []string) (scores []float64, inx int, strict bool) {
|
||||||
|
if c.tfIdf && !c.DidConvertTfIdf {
|
||||||
|
panic("Using a TF-IDF classifier. Please call ConvertTermsFreqToTfIdf before calling ProbScores.")
|
||||||
|
}
|
||||||
|
n := len(c.Classes)
|
||||||
|
scores = make([]float64, n, n)
|
||||||
|
priors := c.getPriors()
|
||||||
|
sum := float64(0)
|
||||||
|
// calculate the score for each class
|
||||||
|
for index, class := range c.Classes {
|
||||||
|
data := c.datas[class]
|
||||||
|
// c is the sum of the logarithms
|
||||||
|
// as outlined in the refresher
|
||||||
|
score := priors[index]
|
||||||
|
for _, word := range doc {
|
||||||
|
score *= data.getWordProb(word)
|
||||||
|
}
|
||||||
|
scores[index] = score
|
||||||
|
sum += score
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
scores[i] /= sum
|
||||||
|
}
|
||||||
|
inx, strict = findMax(scores)
|
||||||
|
atomic.AddInt32(&c.seen, 1)
|
||||||
|
return scores, inx, strict
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeProbScores works the same as ProbScores, but is
|
||||||
|
// able to detect underflow in those cases where underflow
|
||||||
|
// results in the reverse classification. If an underflow is detected,
|
||||||
|
// this method returns an ErrUnderflow, allowing the user to deal with it as
|
||||||
|
// necessary. Note that underflow, under certain rare circumstances,
|
||||||
|
// may still result in incorrect probabilities being returned,
|
||||||
|
// but this method guarantees that all error-less invokations
|
||||||
|
// are properly classified.
|
||||||
|
//
|
||||||
|
// Underflow detection is more costly because it also
|
||||||
|
// has to make additional log score calculations.
|
||||||
|
func (c *Classifier) SafeProbScores(doc []string) (scores []float64, inx int, strict bool, err error) {
|
||||||
|
if c.tfIdf && !c.DidConvertTfIdf {
|
||||||
|
panic("Using a TF-IDF classifier. Please call ConvertTermsFreqToTfIdf before calling SafeProbScores.")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(c.Classes)
|
||||||
|
scores = make([]float64, n, n)
|
||||||
|
logScores := make([]float64, n, n)
|
||||||
|
priors := c.getPriors()
|
||||||
|
sum := float64(0)
|
||||||
|
// calculate the score for each class
|
||||||
|
for index, class := range c.Classes {
|
||||||
|
data := c.datas[class]
|
||||||
|
// c is the sum of the logarithms
|
||||||
|
// as outlined in the refresher
|
||||||
|
score := priors[index]
|
||||||
|
logScore := math.Log(priors[index])
|
||||||
|
for _, word := range doc {
|
||||||
|
p := data.getWordProb(word)
|
||||||
|
score *= p
|
||||||
|
logScore += math.Log(p)
|
||||||
|
}
|
||||||
|
scores[index] = score
|
||||||
|
logScores[index] = logScore
|
||||||
|
sum += score
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
scores[i] /= sum
|
||||||
|
}
|
||||||
|
inx, strict = findMax(scores)
|
||||||
|
logInx, logStrict := findMax(logScores)
|
||||||
|
|
||||||
|
// detect underflow -- the size
|
||||||
|
// relation between scores and logScores
|
||||||
|
// must be preserved or something is wrong
|
||||||
|
if inx != logInx || strict != logStrict {
|
||||||
|
err = ErrUnderflow
|
||||||
|
}
|
||||||
|
atomic.AddInt32(&c.seen, 1)
|
||||||
|
return scores, inx, strict, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordFrequencies returns a matrix of word frequencies that currently
|
||||||
|
// exist in the classifier for each class state for the given input
|
||||||
|
// words. In other words, if you obtain the frequencies
|
||||||
|
//
|
||||||
|
// freqs := c.WordFrequencies(/* [j]string */)
|
||||||
|
//
|
||||||
|
// then the expression freq[i][j] represents the frequency of the j-th
|
||||||
|
// word within the i-th class.
|
||||||
|
func (c *Classifier) WordFrequencies(words []string) (freqMatrix [][]float64) {
|
||||||
|
n, l := len(c.Classes), len(words)
|
||||||
|
freqMatrix = make([][]float64, n)
|
||||||
|
for i := range freqMatrix {
|
||||||
|
arr := make([]float64, l)
|
||||||
|
data := c.datas[c.Classes[i]]
|
||||||
|
for j := range arr {
|
||||||
|
arr[j] = data.getWordProb(words[j])
|
||||||
|
}
|
||||||
|
freqMatrix[i] = arr
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordsByClass returns a map of words and their probability of
|
||||||
|
// appearing in the given class.
|
||||||
|
func (c *Classifier) WordsByClass(class Class) (freqMap map[string]float64) {
|
||||||
|
freqMap = make(map[string]float64)
|
||||||
|
for word, cnt := range c.datas[class].Freqs {
|
||||||
|
freqMap[word] = float64(cnt) / float64(c.datas[class].Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
return freqMap
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// WriteToFile serializes this classifier to a file.
|
||||||
|
func (c *Classifier) WriteToFile(name string) (err error) {
|
||||||
|
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return c.WriteTo(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteClassesToFile writes all classes to files.
|
||||||
|
func (c *Classifier) WriteClassesToFile(rootPath string) (err error) {
|
||||||
|
for name := range c.datas {
|
||||||
|
c.WriteClassToFile(name, rootPath)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteClassToFile writes a single class to file.
|
||||||
|
func (c *Classifier) WriteClassToFile(name Class, rootPath string) (err error) {
|
||||||
|
data := c.datas[name]
|
||||||
|
fileName := filepath.Join(rootPath, string(name))
|
||||||
|
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
enc := gob.NewEncoder(file)
|
||||||
|
err = enc.Encode(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// WriteTo serializes this classifier to GOB and write to Writer.
|
||||||
|
func (c *Classifier) WriteTo(w io.Writer) (err error) {
|
||||||
|
enc := gob.NewEncoder(w)
|
||||||
|
err = enc.Encode(&serializableClassifier{c.Classes, c.learned, int(c.seen), c.datas, c.tfIdf, c.DidConvertTfIdf})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadClassFromFile loads existing class data from a
|
||||||
|
// file.
|
||||||
|
func (c *Classifier) ReadClassFromFile(class Class, location string) (err error) {
|
||||||
|
fileName := filepath.Join(location, string(class))
|
||||||
|
file, err := os.Open(fileName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
dec := gob.NewDecoder(file)
|
||||||
|
w := new(classData)
|
||||||
|
err = dec.Decode(w)
|
||||||
|
|
||||||
|
c.learned++
|
||||||
|
c.datas[class] = w
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// findMax finds the maximum of a set of scores; if the
|
||||||
|
// maximum is strict -- that is, it is the single unique
|
||||||
|
// maximum from the set -- then strict has return value
|
||||||
|
// true. Otherwise it is false.
|
||||||
|
func findMax(scores []float64) (inx int, strict bool) {
|
||||||
|
inx = 0
|
||||||
|
strict = true
|
||||||
|
for i := 1; i < len(scores); i++ {
|
||||||
|
if scores[inx] < scores[i] {
|
||||||
|
inx = i
|
||||||
|
strict = true
|
||||||
|
} else if scores[inx] == scores[i] {
|
||||||
|
strict = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
53
vendor/github.com/jbrukh/bayesian/doc.go
generated
vendored
Normal file
53
vendor/github.com/jbrukh/bayesian/doc.go
generated
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*Package bayesian is a Naive Bayesian Classifier
|
||||||
|
Jake Brukhman <jbrukh@gmail.com>
|
||||||
|
|
||||||
|
BAYESIAN CLASSIFICATION REFRESHER: suppose you have a set
|
||||||
|
of classes (e.g. categories) C := {C_1, ..., C_n}, and a
|
||||||
|
document D consisting of words D := {W_1, ..., W_k}.
|
||||||
|
We wish to ascertain the probability that the document
|
||||||
|
belongs to some class C_j given some set of training data
|
||||||
|
associating documents and classes.
|
||||||
|
|
||||||
|
By Bayes' Theorem, we have that
|
||||||
|
|
||||||
|
P(C_j|D) = P(D|C_j)*P(C_j)/P(D).
|
||||||
|
|
||||||
|
The LHS is the probability that the document belongs to class
|
||||||
|
C_j given the document itself (by which is meant, in practice,
|
||||||
|
the word frequencies occurring in this document), and our program
|
||||||
|
will calculate this probability for each j and spit out the
|
||||||
|
most likely class for this document.
|
||||||
|
|
||||||
|
P(C_j) is referred to as the "prior" probability, or the
|
||||||
|
probability that a document belongs to C_j in general, without
|
||||||
|
seeing the document first. P(D|C_j) is the probability of seeing
|
||||||
|
such a document, given that it belongs to C_j. Here, by assuming
|
||||||
|
that words appear independently in documents (this being the
|
||||||
|
"naive" assumption), we can estimate
|
||||||
|
|
||||||
|
P(D|C_j) ~= P(W_1|C_j)*...*P(W_k|C_j)
|
||||||
|
|
||||||
|
where P(W_i|C_j) is the probability of seeing the given word
|
||||||
|
in a document of the given class. Finally, P(D) can be seen as
|
||||||
|
merely a scaling factor and is not strictly relevant to
|
||||||
|
classificiation, unless you want to normalize the resulting
|
||||||
|
scores and actually see probabilities. In this case, note that
|
||||||
|
|
||||||
|
P(D) = SUM_j(P(D|C_j)*P(C_j))
|
||||||
|
|
||||||
|
One practical issue with performing these calculations is the
|
||||||
|
possibility of float64 underflow when calculating P(D|C_j), as
|
||||||
|
individual word probabilities can be arbitrarily small, and
|
||||||
|
a document can have an arbitrarily large number of them. A
|
||||||
|
typical method for dealing with this case is to transform the
|
||||||
|
probability to the log domain and perform additions instead
|
||||||
|
of multiplications:
|
||||||
|
|
||||||
|
log P(C_j|D) ~ log(P(C_j)) + SUM_i(log P(W_i|C_j))
|
||||||
|
|
||||||
|
where i = 1, ..., k. Note that by doing this, we are discarding
|
||||||
|
the scaling factor P(D) and our scores are no longer
|
||||||
|
probabilities; however, the monotonic relationship of the
|
||||||
|
scores is preserved by the log function.
|
||||||
|
*/
|
||||||
|
package bayesian
|
||||||
3
vendor/github.com/jbrukh/bayesian/todo.txt
generated
vendored
Normal file
3
vendor/github.com/jbrukh/bayesian/todo.txt
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
- revisit underflow detection
|
||||||
|
- add optional smoothing for frequencies
|
||||||
|
- test with drone.io
|
||||||
4
vendor/golang.org/x/crypto/LICENSE
generated
vendored
4
vendor/golang.org/x/crypto/LICENSE
generated
vendored
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
|
|||||||
copyright notice, this list of conditions and the following disclaimer
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
in the documentation and/or other materials provided with the
|
in the documentation and/or other materials provided with the
|
||||||
distribution.
|
distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
* Neither the name of Google LLC nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go
generated
vendored
2
vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go
generated
vendored
@@ -16,7 +16,7 @@ Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To
|
|||||||
choose, you can pass the `New` functions from the different SHA packages to
|
choose, you can pass the `New` functions from the different SHA packages to
|
||||||
pbkdf2.Key.
|
pbkdf2.Key.
|
||||||
*/
|
*/
|
||||||
package pbkdf2 // import "golang.org/x/crypto/pbkdf2"
|
package pbkdf2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
|
|||||||
4
vendor/golang.org/x/exp/LICENSE
generated
vendored
4
vendor/golang.org/x/exp/LICENSE
generated
vendored
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
|
|||||||
copyright notice, this list of conditions and the following disclaimer
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
in the documentation and/or other materials provided with the
|
in the documentation and/or other materials provided with the
|
||||||
distribution.
|
distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
* Neither the name of Google LLC nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
|||||||
50
vendor/golang.org/x/exp/constraints/constraints.go
generated
vendored
50
vendor/golang.org/x/exp/constraints/constraints.go
generated
vendored
@@ -1,50 +0,0 @@
|
|||||||
// Copyright 2021 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 constraints defines a set of useful constraints to be used
|
|
||||||
// with type parameters.
|
|
||||||
package constraints
|
|
||||||
|
|
||||||
// Signed is a constraint that permits any signed integer type.
|
|
||||||
// If future releases of Go add new predeclared signed integer types,
|
|
||||||
// this constraint will be modified to include them.
|
|
||||||
type Signed interface {
|
|
||||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsigned is a constraint that permits any unsigned integer type.
|
|
||||||
// If future releases of Go add new predeclared unsigned integer types,
|
|
||||||
// this constraint will be modified to include them.
|
|
||||||
type Unsigned interface {
|
|
||||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Integer is a constraint that permits any integer type.
|
|
||||||
// If future releases of Go add new predeclared integer types,
|
|
||||||
// this constraint will be modified to include them.
|
|
||||||
type Integer interface {
|
|
||||||
Signed | Unsigned
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float is a constraint that permits any floating-point type.
|
|
||||||
// If future releases of Go add new predeclared floating-point types,
|
|
||||||
// this constraint will be modified to include them.
|
|
||||||
type Float interface {
|
|
||||||
~float32 | ~float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complex is a constraint that permits any complex numeric type.
|
|
||||||
// If future releases of Go add new predeclared complex numeric types,
|
|
||||||
// this constraint will be modified to include them.
|
|
||||||
type Complex interface {
|
|
||||||
~complex64 | ~complex128
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordered is a constraint that permits any ordered type: any type
|
|
||||||
// that supports the operators < <= >= >.
|
|
||||||
// If future releases of Go add new ordered types,
|
|
||||||
// this constraint will be modified to include them.
|
|
||||||
type Ordered interface {
|
|
||||||
Integer | Float | ~string
|
|
||||||
}
|
|
||||||
44
vendor/golang.org/x/exp/slices/cmp.go
generated
vendored
44
vendor/golang.org/x/exp/slices/cmp.go
generated
vendored
@@ -1,44 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
445
vendor/golang.org/x/exp/slices/slices.go
generated
vendored
445
vendor/golang.org/x/exp/slices/slices.go
generated
vendored
@@ -6,9 +6,8 @@
|
|||||||
package slices
|
package slices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"unsafe"
|
"cmp"
|
||||||
|
"slices"
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Equal reports whether two slices are equal: the same length and all
|
// Equal reports whether two slices are equal: the same length and all
|
||||||
@@ -16,16 +15,10 @@ import (
|
|||||||
// Otherwise, the elements are compared in increasing index order, and the
|
// Otherwise, the elements are compared in increasing index order, and the
|
||||||
// comparison stops at the first unequal pair.
|
// comparison stops at the first unequal pair.
|
||||||
// Floating point NaNs are not considered equal.
|
// Floating point NaNs are not considered equal.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Equal[S ~[]E, E comparable](s1, s2 S) bool {
|
func Equal[S ~[]E, E comparable](s1, s2 S) bool {
|
||||||
if len(s1) != len(s2) {
|
return slices.Equal(s1, s2)
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := range s1 {
|
|
||||||
if s1[i] != s2[i] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EqualFunc reports whether two slices are equal using an equality
|
// EqualFunc reports whether two slices are equal using an equality
|
||||||
@@ -33,17 +26,10 @@ func Equal[S ~[]E, E comparable](s1, s2 S) bool {
|
|||||||
// EqualFunc returns false. Otherwise, the elements are compared in
|
// EqualFunc returns false. Otherwise, the elements are compared in
|
||||||
// increasing index order, and the comparison stops at the first index
|
// increasing index order, and the comparison stops at the first index
|
||||||
// for which eq returns false.
|
// for which eq returns false.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, 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 slices.EqualFunc(s1, s2, eq)
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, v1 := range s1 {
|
|
||||||
v2 := s2[i]
|
|
||||||
if !eq(v1, v2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair
|
// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair
|
||||||
@@ -53,20 +39,10 @@ func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, eq func(E1, E2) boo
|
|||||||
// If both slices are equal until one of them ends, the shorter slice is
|
// If both slices are equal until one of them ends, the shorter slice is
|
||||||
// considered less than the longer one.
|
// considered less than the longer one.
|
||||||
// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2.
|
// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2.
|
||||||
func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int {
|
//
|
||||||
for i, v1 := range s1 {
|
//go:fix inline
|
||||||
if i >= len(s2) {
|
func Compare[S ~[]E, E cmp.Ordered](s1, s2 S) int {
|
||||||
return +1
|
return slices.Compare(s1, s2)
|
||||||
}
|
|
||||||
v2 := s2[i]
|
|
||||||
if c := cmpCompare(v1, v2); c != 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s1) < len(s2) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompareFunc is like [Compare] but uses a custom comparison function on each
|
// CompareFunc is like [Compare] but uses a custom comparison function on each
|
||||||
@@ -74,53 +50,41 @@ func Compare[S ~[]E, E constraints.Ordered](s1, s2 S) int {
|
|||||||
// The result is the first non-zero result of cmp; if cmp always
|
// The result is the first non-zero result of cmp; if cmp always
|
||||||
// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2),
|
// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2),
|
||||||
// and +1 if len(s1) > len(s2).
|
// and +1 if len(s1) > len(s2).
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int {
|
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](s1 S1, s2 S2, cmp func(E1, E2) int) int {
|
||||||
for i, v1 := range s1 {
|
return slices.CompareFunc(s1, s2, cmp)
|
||||||
if i >= len(s2) {
|
|
||||||
return +1
|
|
||||||
}
|
|
||||||
v2 := s2[i]
|
|
||||||
if c := cmp(v1, v2); c != 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s1) < len(s2) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index returns the index of the first occurrence of v in s,
|
// Index returns the index of the first occurrence of v in s,
|
||||||
// or -1 if not present.
|
// or -1 if not present.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Index[S ~[]E, E comparable](s S, v E) int {
|
func Index[S ~[]E, E comparable](s S, v E) int {
|
||||||
for i := range s {
|
return slices.Index(s, v)
|
||||||
if v == s[i] {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IndexFunc returns the first index i satisfying f(s[i]),
|
// IndexFunc returns the first index i satisfying f(s[i]),
|
||||||
// or -1 if none do.
|
// or -1 if none do.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
|
||||||
for i := range s {
|
return slices.IndexFunc(s, f)
|
||||||
if f(s[i]) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contains reports whether v is present in s.
|
// Contains reports whether v is present in s.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Contains[S ~[]E, E comparable](s S, v E) bool {
|
func Contains[S ~[]E, E comparable](s S, v E) bool {
|
||||||
return Index(s, v) >= 0
|
return slices.Contains(s, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContainsFunc reports whether at least one
|
// ContainsFunc reports whether at least one
|
||||||
// element e of s satisfies f(e).
|
// element e of s satisfies f(e).
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
|
func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
|
||||||
return IndexFunc(s, f) >= 0
|
return slices.ContainsFunc(s, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert inserts the values v... into s at index i,
|
// Insert inserts the values v... into s at index i,
|
||||||
@@ -130,370 +94,89 @@ func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool {
|
|||||||
// and r[i+len(v)] == value originally at r[i].
|
// and r[i+len(v)] == value originally at r[i].
|
||||||
// Insert panics if i is out of range.
|
// Insert panics if i is out of range.
|
||||||
// This function is O(len(s) + len(v)).
|
// This function is O(len(s) + len(v)).
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Insert[S ~[]E, E any](s S, i int, v ...E) S {
|
func Insert[S ~[]E, E any](s S, i int, v ...E) S {
|
||||||
m := len(v)
|
return slices.Insert(s, i, 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
|
|
||||||
}
|
|
||||||
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 removes the elements s[i:j] from s, returning the modified slice.
|
||||||
// Delete panics if s[i:j] is not a valid slice of s.
|
// Delete panics if j > len(s) or s[i:j] is not a valid slice of s.
|
||||||
// Delete is O(len(s)-j), so if many items must be deleted, it is better to
|
// Delete is O(len(s)-i), so if many items must be deleted, it is better to
|
||||||
// make a single call deleting them all together than to delete one at a time.
|
// make a single call deleting them all together than to delete one at a time.
|
||||||
// Delete might not modify the elements s[len(s)-(j-i):len(s)]. If those
|
// Delete zeroes the elements s[len(s)-(j-i):len(s)].
|
||||||
// elements contain pointers you might consider zeroing those elements so that
|
//
|
||||||
// objects they reference can be garbage collected.
|
//go:fix inline
|
||||||
func Delete[S ~[]E, E any](s S, i, j int) S {
|
func Delete[S ~[]E, E any](s S, i, j int) S {
|
||||||
_ = s[i:j] // bounds check
|
return slices.Delete(s, i, j)
|
||||||
|
|
||||||
return append(s[:i], s[j:]...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteFunc removes any elements from s for which del returns true,
|
// DeleteFunc removes any elements from s for which del returns true,
|
||||||
// returning the modified slice.
|
// returning the modified slice.
|
||||||
// When DeleteFunc removes m elements, it might not modify the elements
|
// DeleteFunc zeroes the elements between the new length and the original length.
|
||||||
// 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
|
//go:fix inline
|
||||||
// collected.
|
|
||||||
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
|
||||||
i := IndexFunc(s, del)
|
return slices.DeleteFunc(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
|
// Replace replaces the elements s[i:j] by the given v, and returns the
|
||||||
// modified slice. Replace panics if s[i:j] is not a valid slice of s.
|
// modified slice. Replace panics if s[i:j] is not a valid slice of s.
|
||||||
|
// When len(v) < (j-i), Replace zeroes the elements between the new length and the original length.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Replace[S ~[]E, E any](s S, i, j int, v ...E) S {
|
func Replace[S ~[]E, E any](s S, i, j int, v ...E) S {
|
||||||
_ = s[i:j] // verify that i:j is a valid subslice
|
return slices.Replace(s, i, j, v...)
|
||||||
|
|
||||||
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) {
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
// Clone returns a copy of the slice.
|
||||||
// The elements are copied using assignment, so this is a shallow clone.
|
// The elements are copied using assignment, so this is a shallow clone.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Clone[S ~[]E, E any](s S) S {
|
func Clone[S ~[]E, E any](s S) S {
|
||||||
// Preserve nil in case it matters.
|
return slices.Clone(s)
|
||||||
if s == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return append(S([]E{}), s...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact replaces consecutive runs of equal elements with a single copy.
|
// Compact replaces consecutive runs of equal elements with a single copy.
|
||||||
// This is like the uniq command found on Unix.
|
// This is like the uniq command found on Unix.
|
||||||
// Compact modifies the contents of the slice s and returns the modified slice,
|
// Compact modifies the contents of the slice s and returns the modified slice,
|
||||||
// which may have a smaller length.
|
// which may have a smaller length.
|
||||||
// When Compact discards m elements in total, it might not modify the elements
|
// Compact zeroes the elements between the new length and the original length.
|
||||||
// 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.
|
//go:fix inline
|
||||||
func Compact[S ~[]E, E comparable](s S) S {
|
func Compact[S ~[]E, E comparable](s S) S {
|
||||||
if len(s) < 2 {
|
return slices.Compact(s)
|
||||||
return s
|
|
||||||
}
|
|
||||||
i := 1
|
|
||||||
for k := 1; k < len(s); k++ {
|
|
||||||
if s[k] != s[k-1] {
|
|
||||||
if i != k {
|
|
||||||
s[i] = s[k]
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s[:i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompactFunc is like [Compact] but uses an equality function to compare elements.
|
// CompactFunc is like [Compact] but uses an equality function to compare elements.
|
||||||
// For runs of elements that compare equal, CompactFunc keeps the first one.
|
// For runs of elements that compare equal, CompactFunc keeps the first one.
|
||||||
|
// CompactFunc zeroes the elements between the new length and the original length.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S {
|
func CompactFunc[S ~[]E, E any](s S, eq func(E, E) bool) S {
|
||||||
if len(s) < 2 {
|
return slices.CompactFunc(s, eq)
|
||||||
return s
|
|
||||||
}
|
|
||||||
i := 1
|
|
||||||
for k := 1; k < len(s); k++ {
|
|
||||||
if !eq(s[k], s[k-1]) {
|
|
||||||
if i != k {
|
|
||||||
s[i] = s[k]
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s[:i]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grow increases the slice's capacity, if necessary, to guarantee space for
|
// Grow increases the slice's capacity, if necessary, to guarantee space for
|
||||||
// another n elements. After Grow(n), at least n elements can be appended
|
// another n elements. After Grow(n), at least n elements can be appended
|
||||||
// to the slice without another allocation. If n is negative or too large to
|
// to the slice without another allocation. If n is negative or too large to
|
||||||
// allocate the memory, Grow panics.
|
// allocate the memory, Grow panics.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Grow[S ~[]E, E any](s S, n int) S {
|
func Grow[S ~[]E, E any](s S, n int) S {
|
||||||
if n < 0 {
|
return slices.Grow(s, n)
|
||||||
panic("cannot be negative")
|
|
||||||
}
|
|
||||||
if n -= cap(s) - len(s); n > 0 {
|
|
||||||
// TODO(https://go.dev/issue/53888): Make using []E instead of S
|
|
||||||
// to workaround a compiler bug where the runtime.growslice optimization
|
|
||||||
// does not take effect. Revert when the compiler is fixed.
|
|
||||||
s = append([]E(s)[:cap(s)], make([]E, n)...)[:len(s)]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clip removes unused capacity from the slice, returning s[:len(s):len(s)].
|
// Clip removes unused capacity from the slice, returning s[:len(s):len(s)].
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Clip[S ~[]E, E any](s S) S {
|
func Clip[S ~[]E, E any](s S) S {
|
||||||
return s[:len(s):len(s)]
|
return slices.Clip(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.
|
// Reverse reverses the elements of the slice in place.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func Reverse[S ~[]E, E any](s S) {
|
func Reverse[S ~[]E, E any](s S) {
|
||||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
slices.Reverse(s)
|
||||||
s[i], s[j] = s[j], s[i]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
166
vendor/golang.org/x/exp/slices/sort.go
generated
vendored
166
vendor/golang.org/x/exp/slices/sort.go
generated
vendored
@@ -2,143 +2,103 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:generate go run $GOROOT/src/sort/gen_sort_variants.go -exp
|
|
||||||
|
|
||||||
package slices
|
package slices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/bits"
|
"cmp"
|
||||||
|
"slices"
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sort sorts a slice of any ordered type in ascending order.
|
// Sort sorts a slice of any ordered type in ascending order.
|
||||||
// When sorting floating-point numbers, NaNs are ordered before other values.
|
// When sorting floating-point numbers, NaNs are ordered before other values.
|
||||||
func Sort[S ~[]E, E constraints.Ordered](x S) {
|
//
|
||||||
n := len(x)
|
//go:fix inline
|
||||||
pdqsortOrdered(x, 0, n, bits.Len(uint(n)))
|
func Sort[S ~[]E, E cmp.Ordered](x S) {
|
||||||
|
slices.Sort(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortFunc sorts the slice x in ascending order as determined by the cmp
|
// SortFunc sorts the slice x in ascending order as determined by the cmp
|
||||||
// function. This sort is not guaranteed to be stable.
|
// function. This sort is not guaranteed to be stable.
|
||||||
// cmp(a, b) should return a negative number when a < b, a positive number when
|
// cmp(a, b) should return a negative number when a < b, a positive number when
|
||||||
// a > b and zero when a == b.
|
// a > b and zero when a == b or when a is not comparable to b in the sense
|
||||||
|
// of the formal definition of Strict Weak Ordering.
|
||||||
//
|
//
|
||||||
// SortFunc requires that cmp is a strict weak ordering.
|
// SortFunc requires that cmp is a strict weak ordering.
|
||||||
// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings.
|
// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings.
|
||||||
|
// To indicate 'uncomparable', return 0 from the function.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
||||||
n := len(x)
|
slices.SortFunc(x, cmp)
|
||||||
pdqsortCmpFunc(x, 0, n, bits.Len(uint(n)), cmp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortStableFunc sorts the slice x while keeping the original order of equal
|
// SortStableFunc sorts the slice x while keeping the original order of equal
|
||||||
// elements, using cmp to compare elements in the same way as [SortFunc].
|
// elements, using cmp to compare elements in the same way as [SortFunc].
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) {
|
||||||
stableCmpFunc(x, len(x), cmp)
|
slices.SortStableFunc(x, cmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSorted reports whether x is sorted in ascending order.
|
// IsSorted reports whether x is sorted in ascending order.
|
||||||
func IsSorted[S ~[]E, E constraints.Ordered](x S) bool {
|
//
|
||||||
for i := len(x) - 1; i > 0; i-- {
|
//go:fix inline
|
||||||
if cmpLess(x[i], x[i-1]) {
|
func IsSorted[S ~[]E, E cmp.Ordered](x S) bool {
|
||||||
return false
|
return slices.IsSorted(x)
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the
|
// IsSortedFunc reports whether x is sorted in ascending order, with cmp as the
|
||||||
// comparison function as defined by [SortFunc].
|
// comparison function as defined by [SortFunc].
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool {
|
func IsSortedFunc[S ~[]E, E any](x S, cmp func(a, b E) int) bool {
|
||||||
for i := len(x) - 1; i > 0; i-- {
|
return slices.IsSortedFunc(x, cmp)
|
||||||
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.
|
// 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
|
// For floating-point numbers, Min propagates NaNs (any NaN value in x
|
||||||
// forces the output to be NaN).
|
// forces the output to be NaN).
|
||||||
func Min[S ~[]E, E constraints.Ordered](x S) E {
|
//
|
||||||
if len(x) < 1 {
|
//go:fix inline
|
||||||
panic("slices.Min: empty list")
|
func Min[S ~[]E, E cmp.Ordered](x S) E {
|
||||||
}
|
return slices.Min(x)
|
||||||
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.
|
// 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
|
// It panics if x is empty. If there is more than one minimal element
|
||||||
// according to the cmp function, MinFunc returns the first one.
|
// according to the cmp function, MinFunc returns the first one.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
|
func MinFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
|
||||||
if len(x) < 1 {
|
return slices.MinFunc(x, cmp)
|
||||||
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.
|
// 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
|
// For floating-point E, Max propagates NaNs (any NaN value in x
|
||||||
// forces the output to be NaN).
|
// forces the output to be NaN).
|
||||||
func Max[S ~[]E, E constraints.Ordered](x S) E {
|
//
|
||||||
if len(x) < 1 {
|
//go:fix inline
|
||||||
panic("slices.Max: empty list")
|
func Max[S ~[]E, E cmp.Ordered](x S) E {
|
||||||
}
|
return slices.Max(x)
|
||||||
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.
|
// 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
|
// It panics if x is empty. If there is more than one maximal element
|
||||||
// according to the cmp function, MaxFunc returns the first one.
|
// according to the cmp function, MaxFunc returns the first one.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
|
func MaxFunc[S ~[]E, E any](x S, cmp func(a, b E) int) E {
|
||||||
if len(x) < 1 {
|
return slices.MaxFunc(x, cmp)
|
||||||
panic("slices.MaxFunc: empty list")
|
|
||||||
}
|
|
||||||
m := x[0]
|
|
||||||
for i := 1; i < len(x); i++ {
|
|
||||||
if cmp(x[i], m) > 0 {
|
|
||||||
m = x[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BinarySearch searches for target in a sorted slice and returns the position
|
// BinarySearch searches for target in a sorted slice and returns the position
|
||||||
// where target is found, or the position where target would appear in the
|
// where target is found, or the position where target would appear in the
|
||||||
// sort order; it also returns a bool saying whether the target is really found
|
// sort order; it also returns a bool saying whether the target is really found
|
||||||
// in the slice. The slice must be sorted in increasing order.
|
// in the slice. The slice must be sorted in increasing order.
|
||||||
func BinarySearch[S ~[]E, E constraints.Ordered](x S, target E) (int, bool) {
|
//
|
||||||
// Inlining is faster than calling BinarySearchFunc with a lambda.
|
//go:fix inline
|
||||||
n := len(x)
|
func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool) {
|
||||||
// Define x[-1] < target and x[n] >= target.
|
return slices.BinarySearch(x, target)
|
||||||
// Invariant: x[i-1] < target, x[j] >= target.
|
|
||||||
i, j := 0, n
|
|
||||||
for i < j {
|
|
||||||
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
|
||||||
// i ≤ h < j
|
|
||||||
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 || (isNaN(x[i]) && isNaN(target)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BinarySearchFunc works like [BinarySearch], but uses a custom comparison
|
// BinarySearchFunc works like [BinarySearch], but uses a custom comparison
|
||||||
@@ -148,48 +108,8 @@ func BinarySearch[S ~[]E, E constraints.Ordered](x S, target E) (int, bool) {
|
|||||||
// or a positive number if the slice element follows the target.
|
// or a positive number if the slice element follows the target.
|
||||||
// cmp must implement the same ordering as the slice, such that if
|
// cmp must implement the same ordering as the slice, such that if
|
||||||
// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice.
|
// cmp(a, t) < 0 and cmp(b, t) >= 0, then a must precede b in the slice.
|
||||||
|
//
|
||||||
|
//go:fix inline
|
||||||
func BinarySearchFunc[S ~[]E, E, T any](x S, 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)
|
return slices.BinarySearchFunc(x, target, cmp)
|
||||||
// Define cmp(x[-1], target) < 0 and cmp(x[n], target) >= 0 .
|
|
||||||
// Invariant: cmp(x[i - 1], target) < 0, cmp(x[j], target) >= 0.
|
|
||||||
i, j := 0, n
|
|
||||||
for i < j {
|
|
||||||
h := int(uint(i+j) >> 1) // avoid overflow when computing h
|
|
||||||
// i ≤ h < j
|
|
||||||
if cmp(x[h], target) < 0 {
|
|
||||||
i = h + 1 // preserves cmp(x[i - 1], target) < 0
|
|
||||||
} else {
|
|
||||||
j = h // preserves cmp(x[j], target) >= 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// i == j, cmp(x[i-1], target) < 0, and cmp(x[j], target) (= cmp(x[i], target)) >= 0 => answer is i.
|
|
||||||
return i, i < n && cmp(x[i], target) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type sortedHint int // hint for pdqsort when choosing the pivot
|
|
||||||
|
|
||||||
const (
|
|
||||||
unknownHint sortedHint = iota
|
|
||||||
increasingHint
|
|
||||||
decreasingHint
|
|
||||||
)
|
|
||||||
|
|
||||||
// xorshift paper: https://www.jstatsoft.org/article/view/v008i14/xorshift.pdf
|
|
||||||
type xorshift uint64
|
|
||||||
|
|
||||||
func (r *xorshift) Next() uint64 {
|
|
||||||
*r ^= *r << 13
|
|
||||||
*r ^= *r >> 17
|
|
||||||
*r ^= *r << 5
|
|
||||||
return uint64(*r)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
479
vendor/golang.org/x/exp/slices/zsortanyfunc.go
generated
vendored
479
vendor/golang.org/x/exp/slices/zsortanyfunc.go
generated
vendored
@@ -1,479 +0,0 @@
|
|||||||
// Code generated by gen_sort_variants.go; DO NOT EDIT.
|
|
||||||
|
|
||||||
// Copyright 2022 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
|
|
||||||
|
|
||||||
// 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 && (cmp(data[j], data[j-1]) < 0); j-- {
|
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// siftDownCmpFunc implements the heap property on data[lo:hi].
|
|
||||||
// first is an offset into the array where the root of the heap lies.
|
|
||||||
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 && (cmp(data[first+child], data[first+child+1]) < 0) {
|
|
||||||
child++
|
|
||||||
}
|
|
||||||
if !(cmp(data[first+root], data[first+child]) < 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data[first+root], data[first+child] = data[first+child], data[first+root]
|
|
||||||
root = child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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-- {
|
|
||||||
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]
|
|
||||||
siftDownCmpFunc(data, lo, i, first, cmp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 pdqsortCmpFunc[E any](data []E, a, b, limit int, cmp func(a, b E) int) {
|
|
||||||
const maxInsertion = 12
|
|
||||||
|
|
||||||
var (
|
|
||||||
wasBalanced = true // whether the last partitioning was reasonably balanced
|
|
||||||
wasPartitioned = true // whether the slice was already partitioned
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
length := b - a
|
|
||||||
|
|
||||||
if length <= maxInsertion {
|
|
||||||
insertionSortCmpFunc(data, a, b, cmp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to heapsort if too many bad choices were made.
|
|
||||||
if limit == 0 {
|
|
||||||
heapSortCmpFunc(data, a, b, cmp)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last partitioning was imbalanced, we need to breaking patterns.
|
|
||||||
if !wasBalanced {
|
|
||||||
breakPatternsCmpFunc(data, a, b, cmp)
|
|
||||||
limit--
|
|
||||||
}
|
|
||||||
|
|
||||||
pivot, hint := choosePivotCmpFunc(data, a, b, cmp)
|
|
||||||
if hint == decreasingHint {
|
|
||||||
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.
|
|
||||||
pivot = (b - 1) - (pivot - a)
|
|
||||||
hint = increasingHint
|
|
||||||
}
|
|
||||||
|
|
||||||
// The slice is likely already sorted.
|
|
||||||
if wasBalanced && wasPartitioned && hint == increasingHint {
|
|
||||||
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 && !(cmp(data[a-1], data[pivot]) < 0) {
|
|
||||||
mid := partitionEqualCmpFunc(data, a, b, pivot, cmp)
|
|
||||||
a = mid
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
pdqsortCmpFunc(data, a, mid, limit, cmp)
|
|
||||||
a = mid + 1
|
|
||||||
} else {
|
|
||||||
wasBalanced = rightLen >= balanceThreshold
|
|
||||||
pdqsortCmpFunc(data, mid+1, b, limit, cmp)
|
|
||||||
b = mid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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 && (cmp(data[i], data[a]) < 0) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
for i <= j && !(cmp(data[j], data[a]) < 0) {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if i > j {
|
|
||||||
data[j], data[a] = data[a], data[j]
|
|
||||||
return j, true
|
|
||||||
}
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
|
|
||||||
for {
|
|
||||||
for i <= j && (cmp(data[i], data[a]) < 0) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
for i <= j && !(cmp(data[j], data[a]) < 0) {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if i > j {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
data[j], data[a] = data[a], data[j]
|
|
||||||
return j, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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 && !(cmp(data[a], data[i]) < 0) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
for i <= j && (cmp(data[a], data[j]) < 0) {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if i > j {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 && !(cmp(data[i], data[i-1]) < 0) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if b-a < shortestShifting {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
data[i], data[i-1] = data[i-1], data[i]
|
|
||||||
|
|
||||||
// Shift the smaller one to the left.
|
|
||||||
if i-a >= 2 {
|
|
||||||
for j := i - 1; j >= 1; j-- {
|
|
||||||
if !(cmp(data[j], data[j-1]) < 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Shift the greater one to the right.
|
|
||||||
if b-i >= 2 {
|
|
||||||
for j := i + 1; j < b; j++ {
|
|
||||||
if !(cmp(data[j], data[j-1]) < 0) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// breakPatternsCmpFunc scatters some elements around in an attempt to break some patterns
|
|
||||||
// that might cause imbalanced partitions in quicksort.
|
|
||||||
func breakPatternsCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
|
||||||
length := b - a
|
|
||||||
if length >= 8 {
|
|
||||||
random := xorshift(length)
|
|
||||||
modulus := nextPowerOfTwo(length)
|
|
||||||
|
|
||||||
for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ {
|
|
||||||
other := int(uint(random.Next()) & (modulus - 1))
|
|
||||||
if other >= length {
|
|
||||||
other -= length
|
|
||||||
}
|
|
||||||
data[idx], data[a+other] = data[a+other], data[idx]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 choosePivotCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) (pivot int, hint sortedHint) {
|
|
||||||
const (
|
|
||||||
shortestNinther = 50
|
|
||||||
maxSwaps = 4 * 3
|
|
||||||
)
|
|
||||||
|
|
||||||
l := b - a
|
|
||||||
|
|
||||||
var (
|
|
||||||
swaps int
|
|
||||||
i = a + l/4*1
|
|
||||||
j = a + l/4*2
|
|
||||||
k = a + l/4*3
|
|
||||||
)
|
|
||||||
|
|
||||||
if l >= 8 {
|
|
||||||
if l >= shortestNinther {
|
|
||||||
// Tukey ninther method, the idea came from Rust's implementation.
|
|
||||||
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 = medianCmpFunc(data, i, j, k, &swaps, cmp)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch swaps {
|
|
||||||
case 0:
|
|
||||||
return j, increasingHint
|
|
||||||
case maxSwaps:
|
|
||||||
return j, decreasingHint
|
|
||||||
default:
|
|
||||||
return j, unknownHint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 reverseRangeCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) {
|
|
||||||
i := a
|
|
||||||
j := b - 1
|
|
||||||
for i < j {
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 {
|
|
||||||
insertionSortCmpFunc(data, a, b, cmp)
|
|
||||||
a = b
|
|
||||||
b += blockSize
|
|
||||||
}
|
|
||||||
insertionSortCmpFunc(data, a, n, cmp)
|
|
||||||
|
|
||||||
for blockSize < n {
|
|
||||||
a, b = 0, 2*blockSize
|
|
||||||
for b <= n {
|
|
||||||
symMergeCmpFunc(data, a, a+blockSize, b, cmp)
|
|
||||||
a = b
|
|
||||||
b += 2 * blockSize
|
|
||||||
}
|
|
||||||
if m := a + blockSize; m < n {
|
|
||||||
symMergeCmpFunc(data, a, m, n, cmp)
|
|
||||||
}
|
|
||||||
blockSize *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// Computer Science, pages 714-723. Springer, 2004.
|
|
||||||
//
|
|
||||||
// Let M = m-a and N = b-n. Wolog M < N.
|
|
||||||
// The recursion depth is bound by ceil(log(N+M)).
|
|
||||||
// The algorithm needs O(M*log(N/M + 1)) calls to data.Less.
|
|
||||||
// The algorithm needs O((M+N)*log(M)) calls to data.Swap.
|
|
||||||
//
|
|
||||||
// The paper gives O((M+N)*log(M)) as the number of assignments assuming a
|
|
||||||
// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation
|
|
||||||
// in the paper carries through for Swap operations, especially as the block
|
|
||||||
// swapping rotate uses only O(M+N) Swaps.
|
|
||||||
//
|
|
||||||
// symMerge assumes non-degenerate arguments: a < m && m < b.
|
|
||||||
// Having the caller check this condition eliminates many leaf recursion calls,
|
|
||||||
// which improves performance.
|
|
||||||
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.
|
|
||||||
if m-a == 1 {
|
|
||||||
// Use binary search to find the lowest index i
|
|
||||||
// such that data[i] >= data[a] for m <= i < b.
|
|
||||||
// Exit the search loop with i == b in case no such index exists.
|
|
||||||
i := m
|
|
||||||
j := b
|
|
||||||
for i < j {
|
|
||||||
h := int(uint(i+j) >> 1)
|
|
||||||
if cmp(data[h], data[a]) < 0 {
|
|
||||||
i = h + 1
|
|
||||||
} else {
|
|
||||||
j = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Swap values until data[a] reaches the position before i.
|
|
||||||
for k := a; k < i-1; k++ {
|
|
||||||
data[k], data[k+1] = data[k+1], data[k]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid unnecessary recursions of symMerge
|
|
||||||
// by direct insertion of data[m] into data[a:m]
|
|
||||||
// if data[m:b] only contains one element.
|
|
||||||
if b-m == 1 {
|
|
||||||
// Use binary search to find the lowest index i
|
|
||||||
// such that data[i] > data[m] for a <= i < m.
|
|
||||||
// Exit the search loop with i == m in case no such index exists.
|
|
||||||
i := a
|
|
||||||
j := m
|
|
||||||
for i < j {
|
|
||||||
h := int(uint(i+j) >> 1)
|
|
||||||
if !(cmp(data[m], data[h]) < 0) {
|
|
||||||
i = h + 1
|
|
||||||
} else {
|
|
||||||
j = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Swap values until data[m] reaches the position i.
|
|
||||||
for k := m; k > i; k-- {
|
|
||||||
data[k], data[k-1] = data[k-1], data[k]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mid := int(uint(a+b) >> 1)
|
|
||||||
n := mid + m
|
|
||||||
var start, r int
|
|
||||||
if m > mid {
|
|
||||||
start = n - b
|
|
||||||
r = mid
|
|
||||||
} else {
|
|
||||||
start = a
|
|
||||||
r = m
|
|
||||||
}
|
|
||||||
p := n - 1
|
|
||||||
|
|
||||||
for start < r {
|
|
||||||
c := int(uint(start+r) >> 1)
|
|
||||||
if !(cmp(data[p-c], data[c]) < 0) {
|
|
||||||
start = c + 1
|
|
||||||
} else {
|
|
||||||
r = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
end := n - start
|
|
||||||
if start < m && m < end {
|
|
||||||
rotateCmpFunc(data, start, m, end, cmp)
|
|
||||||
}
|
|
||||||
if a < start && start < mid {
|
|
||||||
symMergeCmpFunc(data, a, start, mid, cmp)
|
|
||||||
}
|
|
||||||
if mid < end && end < b {
|
|
||||||
symMergeCmpFunc(data, mid, end, b, cmp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 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 {
|
|
||||||
swapRangeCmpFunc(data, m-i, m, j, cmp)
|
|
||||||
i -= j
|
|
||||||
} else {
|
|
||||||
swapRangeCmpFunc(data, m-i, m+j-i, i, cmp)
|
|
||||||
j -= i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// i == j
|
|
||||||
swapRangeCmpFunc(data, m-i, m, i, cmp)
|
|
||||||
}
|
|
||||||
481
vendor/golang.org/x/exp/slices/zsortordered.go
generated
vendored
481
vendor/golang.org/x/exp/slices/zsortordered.go
generated
vendored
@@ -1,481 +0,0 @@
|
|||||||
// Code generated by gen_sort_variants.go; DO NOT EDIT.
|
|
||||||
|
|
||||||
// Copyright 2022 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"
|
|
||||||
|
|
||||||
// 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 && cmpLess(data[j], data[j-1]); j-- {
|
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// siftDownOrdered implements the heap property on data[lo:hi].
|
|
||||||
// first is an offset into the array where the root of the heap lies.
|
|
||||||
func siftDownOrdered[E constraints.Ordered](data []E, lo, hi, first int) {
|
|
||||||
root := lo
|
|
||||||
for {
|
|
||||||
child := 2*root + 1
|
|
||||||
if child >= hi {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if child+1 < hi && cmpLess(data[first+child], data[first+child+1]) {
|
|
||||||
child++
|
|
||||||
}
|
|
||||||
if !cmpLess(data[first+root], data[first+child]) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data[first+root], data[first+child] = data[first+child], data[first+root]
|
|
||||||
root = child
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func heapSortOrdered[E constraints.Ordered](data []E, a, b int) {
|
|
||||||
first := a
|
|
||||||
lo := 0
|
|
||||||
hi := b - a
|
|
||||||
|
|
||||||
// Build heap with greatest element at top.
|
|
||||||
for i := (hi - 1) / 2; i >= 0; i-- {
|
|
||||||
siftDownOrdered(data, i, hi, first)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]
|
|
||||||
siftDownOrdered(data, lo, i, first)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// pdqsortOrdered 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 pdqsortOrdered[E constraints.Ordered](data []E, a, b, limit int) {
|
|
||||||
const maxInsertion = 12
|
|
||||||
|
|
||||||
var (
|
|
||||||
wasBalanced = true // whether the last partitioning was reasonably balanced
|
|
||||||
wasPartitioned = true // whether the slice was already partitioned
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
length := b - a
|
|
||||||
|
|
||||||
if length <= maxInsertion {
|
|
||||||
insertionSortOrdered(data, a, b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to heapsort if too many bad choices were made.
|
|
||||||
if limit == 0 {
|
|
||||||
heapSortOrdered(data, a, b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the last partitioning was imbalanced, we need to breaking patterns.
|
|
||||||
if !wasBalanced {
|
|
||||||
breakPatternsOrdered(data, a, b)
|
|
||||||
limit--
|
|
||||||
}
|
|
||||||
|
|
||||||
pivot, hint := choosePivotOrdered(data, a, b)
|
|
||||||
if hint == decreasingHint {
|
|
||||||
reverseRangeOrdered(data, a, b)
|
|
||||||
// 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.
|
|
||||||
pivot = (b - 1) - (pivot - a)
|
|
||||||
hint = increasingHint
|
|
||||||
}
|
|
||||||
|
|
||||||
// The slice is likely already sorted.
|
|
||||||
if wasBalanced && wasPartitioned && hint == increasingHint {
|
|
||||||
if partialInsertionSortOrdered(data, a, b) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probably the slice contains many duplicate elements, partition the slice into
|
|
||||||
// elements equal to and elements greater than the pivot.
|
|
||||||
if a > 0 && !cmpLess(data[a-1], data[pivot]) {
|
|
||||||
mid := partitionEqualOrdered(data, a, b, pivot)
|
|
||||||
a = mid
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mid, alreadyPartitioned := partitionOrdered(data, a, b, pivot)
|
|
||||||
wasPartitioned = alreadyPartitioned
|
|
||||||
|
|
||||||
leftLen, rightLen := mid-a, b-mid
|
|
||||||
balanceThreshold := length / 8
|
|
||||||
if leftLen < rightLen {
|
|
||||||
wasBalanced = leftLen >= balanceThreshold
|
|
||||||
pdqsortOrdered(data, a, mid, limit)
|
|
||||||
a = mid + 1
|
|
||||||
} else {
|
|
||||||
wasBalanced = rightLen >= balanceThreshold
|
|
||||||
pdqsortOrdered(data, mid+1, b, limit)
|
|
||||||
b = mid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// partitionOrdered 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 partitionOrdered[E constraints.Ordered](data []E, a, b, pivot 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 && cmpLess(data[i], data[a]) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
for i <= j && !cmpLess(data[j], data[a]) {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if i > j {
|
|
||||||
data[j], data[a] = data[a], data[j]
|
|
||||||
return j, true
|
|
||||||
}
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
|
|
||||||
for {
|
|
||||||
for i <= j && cmpLess(data[i], data[a]) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
for i <= j && !cmpLess(data[j], data[a]) {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if i > j {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
data[j], data[a] = data[a], data[j]
|
|
||||||
return j, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// partitionEqualOrdered 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 partitionEqualOrdered[E constraints.Ordered](data []E, a, b, pivot 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 && !cmpLess(data[a], data[i]) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
for i <= j && cmpLess(data[a], data[j]) {
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
if i > j {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// partialInsertionSortOrdered partially sorts a slice, returns true if the slice is sorted at the end.
|
|
||||||
func partialInsertionSortOrdered[E constraints.Ordered](data []E, a, b 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 && !cmpLess(data[i], data[i-1]) {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
if i == b {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if b-a < shortestShifting {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
data[i], data[i-1] = data[i-1], data[i]
|
|
||||||
|
|
||||||
// Shift the smaller one to the left.
|
|
||||||
if i-a >= 2 {
|
|
||||||
for j := i - 1; j >= 1; j-- {
|
|
||||||
if !cmpLess(data[j], data[j-1]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Shift the greater one to the right.
|
|
||||||
if b-i >= 2 {
|
|
||||||
for j := i + 1; j < b; j++ {
|
|
||||||
if !cmpLess(data[j], data[j-1]) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
data[j], data[j-1] = data[j-1], data[j]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// breakPatternsOrdered scatters some elements around in an attempt to break some patterns
|
|
||||||
// that might cause imbalanced partitions in quicksort.
|
|
||||||
func breakPatternsOrdered[E constraints.Ordered](data []E, a, b int) {
|
|
||||||
length := b - a
|
|
||||||
if length >= 8 {
|
|
||||||
random := xorshift(length)
|
|
||||||
modulus := nextPowerOfTwo(length)
|
|
||||||
|
|
||||||
for idx := a + (length/4)*2 - 1; idx <= a+(length/4)*2+1; idx++ {
|
|
||||||
other := int(uint(random.Next()) & (modulus - 1))
|
|
||||||
if other >= length {
|
|
||||||
other -= length
|
|
||||||
}
|
|
||||||
data[idx], data[a+other] = data[a+other], data[idx]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// choosePivotOrdered 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 choosePivotOrdered[E constraints.Ordered](data []E, a, b int) (pivot int, hint sortedHint) {
|
|
||||||
const (
|
|
||||||
shortestNinther = 50
|
|
||||||
maxSwaps = 4 * 3
|
|
||||||
)
|
|
||||||
|
|
||||||
l := b - a
|
|
||||||
|
|
||||||
var (
|
|
||||||
swaps int
|
|
||||||
i = a + l/4*1
|
|
||||||
j = a + l/4*2
|
|
||||||
k = a + l/4*3
|
|
||||||
)
|
|
||||||
|
|
||||||
if l >= 8 {
|
|
||||||
if l >= shortestNinther {
|
|
||||||
// Tukey ninther method, the idea came from Rust's implementation.
|
|
||||||
i = medianAdjacentOrdered(data, i, &swaps)
|
|
||||||
j = medianAdjacentOrdered(data, j, &swaps)
|
|
||||||
k = medianAdjacentOrdered(data, k, &swaps)
|
|
||||||
}
|
|
||||||
// Find the median among i, j, k and stores it into j.
|
|
||||||
j = medianOrdered(data, i, j, k, &swaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch swaps {
|
|
||||||
case 0:
|
|
||||||
return j, increasingHint
|
|
||||||
case maxSwaps:
|
|
||||||
return j, decreasingHint
|
|
||||||
default:
|
|
||||||
return j, unknownHint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 cmpLess(data[b], data[a]) {
|
|
||||||
*swaps++
|
|
||||||
return b, a
|
|
||||||
}
|
|
||||||
return a, b
|
|
||||||
}
|
|
||||||
|
|
||||||
// medianOrdered returns x where data[x] is the median of data[a],data[b],data[c], where x is a, b, or c.
|
|
||||||
func medianOrdered[E constraints.Ordered](data []E, a, b, c int, swaps *int) int {
|
|
||||||
a, b = order2Ordered(data, a, b, swaps)
|
|
||||||
b, c = order2Ordered(data, b, c, swaps)
|
|
||||||
a, b = order2Ordered(data, a, b, swaps)
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// medianAdjacentOrdered finds the median of data[a - 1], data[a], data[a + 1] and stores the index into a.
|
|
||||||
func medianAdjacentOrdered[E constraints.Ordered](data []E, a int, swaps *int) int {
|
|
||||||
return medianOrdered(data, a-1, a, a+1, swaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
func reverseRangeOrdered[E constraints.Ordered](data []E, a, b int) {
|
|
||||||
i := a
|
|
||||||
j := b - 1
|
|
||||||
for i < j {
|
|
||||||
data[i], data[j] = data[j], data[i]
|
|
||||||
i++
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func swapRangeOrdered[E constraints.Ordered](data []E, a, b, n int) {
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
data[a+i], data[b+i] = data[b+i], data[a+i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func stableOrdered[E constraints.Ordered](data []E, n int) {
|
|
||||||
blockSize := 20 // must be > 0
|
|
||||||
a, b := 0, blockSize
|
|
||||||
for b <= n {
|
|
||||||
insertionSortOrdered(data, a, b)
|
|
||||||
a = b
|
|
||||||
b += blockSize
|
|
||||||
}
|
|
||||||
insertionSortOrdered(data, a, n)
|
|
||||||
|
|
||||||
for blockSize < n {
|
|
||||||
a, b = 0, 2*blockSize
|
|
||||||
for b <= n {
|
|
||||||
symMergeOrdered(data, a, a+blockSize, b)
|
|
||||||
a = b
|
|
||||||
b += 2 * blockSize
|
|
||||||
}
|
|
||||||
if m := a + blockSize; m < n {
|
|
||||||
symMergeOrdered(data, a, m, n)
|
|
||||||
}
|
|
||||||
blockSize *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// symMergeOrdered 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
|
|
||||||
// Computer Science, pages 714-723. Springer, 2004.
|
|
||||||
//
|
|
||||||
// Let M = m-a and N = b-n. Wolog M < N.
|
|
||||||
// The recursion depth is bound by ceil(log(N+M)).
|
|
||||||
// The algorithm needs O(M*log(N/M + 1)) calls to data.Less.
|
|
||||||
// The algorithm needs O((M+N)*log(M)) calls to data.Swap.
|
|
||||||
//
|
|
||||||
// The paper gives O((M+N)*log(M)) as the number of assignments assuming a
|
|
||||||
// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation
|
|
||||||
// in the paper carries through for Swap operations, especially as the block
|
|
||||||
// swapping rotate uses only O(M+N) Swaps.
|
|
||||||
//
|
|
||||||
// symMerge assumes non-degenerate arguments: a < m && m < b.
|
|
||||||
// Having the caller check this condition eliminates many leaf recursion calls,
|
|
||||||
// which improves performance.
|
|
||||||
func symMergeOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
|
||||||
// Avoid unnecessary recursions of symMerge
|
|
||||||
// by direct insertion of data[a] into data[m:b]
|
|
||||||
// if data[a:m] only contains one element.
|
|
||||||
if m-a == 1 {
|
|
||||||
// Use binary search to find the lowest index i
|
|
||||||
// such that data[i] >= data[a] for m <= i < b.
|
|
||||||
// Exit the search loop with i == b in case no such index exists.
|
|
||||||
i := m
|
|
||||||
j := b
|
|
||||||
for i < j {
|
|
||||||
h := int(uint(i+j) >> 1)
|
|
||||||
if cmpLess(data[h], data[a]) {
|
|
||||||
i = h + 1
|
|
||||||
} else {
|
|
||||||
j = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Swap values until data[a] reaches the position before i.
|
|
||||||
for k := a; k < i-1; k++ {
|
|
||||||
data[k], data[k+1] = data[k+1], data[k]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid unnecessary recursions of symMerge
|
|
||||||
// by direct insertion of data[m] into data[a:m]
|
|
||||||
// if data[m:b] only contains one element.
|
|
||||||
if b-m == 1 {
|
|
||||||
// Use binary search to find the lowest index i
|
|
||||||
// such that data[i] > data[m] for a <= i < m.
|
|
||||||
// Exit the search loop with i == m in case no such index exists.
|
|
||||||
i := a
|
|
||||||
j := m
|
|
||||||
for i < j {
|
|
||||||
h := int(uint(i+j) >> 1)
|
|
||||||
if !cmpLess(data[m], data[h]) {
|
|
||||||
i = h + 1
|
|
||||||
} else {
|
|
||||||
j = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Swap values until data[m] reaches the position i.
|
|
||||||
for k := m; k > i; k-- {
|
|
||||||
data[k], data[k-1] = data[k-1], data[k]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mid := int(uint(a+b) >> 1)
|
|
||||||
n := mid + m
|
|
||||||
var start, r int
|
|
||||||
if m > mid {
|
|
||||||
start = n - b
|
|
||||||
r = mid
|
|
||||||
} else {
|
|
||||||
start = a
|
|
||||||
r = m
|
|
||||||
}
|
|
||||||
p := n - 1
|
|
||||||
|
|
||||||
for start < r {
|
|
||||||
c := int(uint(start+r) >> 1)
|
|
||||||
if !cmpLess(data[p-c], data[c]) {
|
|
||||||
start = c + 1
|
|
||||||
} else {
|
|
||||||
r = c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
end := n - start
|
|
||||||
if start < m && m < end {
|
|
||||||
rotateOrdered(data, start, m, end)
|
|
||||||
}
|
|
||||||
if a < start && start < mid {
|
|
||||||
symMergeOrdered(data, a, start, mid)
|
|
||||||
}
|
|
||||||
if mid < end && end < b {
|
|
||||||
symMergeOrdered(data, mid, end, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rotateOrdered 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 rotateOrdered[E constraints.Ordered](data []E, a, m, b int) {
|
|
||||||
i := m - a
|
|
||||||
j := b - m
|
|
||||||
|
|
||||||
for i != j {
|
|
||||||
if i > j {
|
|
||||||
swapRangeOrdered(data, m-i, m, j)
|
|
||||||
i -= j
|
|
||||||
} else {
|
|
||||||
swapRangeOrdered(data, m-i, m+j-i, i)
|
|
||||||
j -= i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// i == j
|
|
||||||
swapRangeOrdered(data, m-i, m, i)
|
|
||||||
}
|
|
||||||
27
vendor/golang.org/x/net/LICENSE
generated
vendored
Normal file
27
vendor/golang.org/x/net/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google LLC nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
22
vendor/golang.org/x/net/PATENTS
generated
vendored
Normal file
22
vendor/golang.org/x/net/PATENTS
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
Additional IP Rights Grant (Patents)
|
||||||
|
|
||||||
|
"This implementation" means the copyrightable works distributed by
|
||||||
|
Google as part of the Go project.
|
||||||
|
|
||||||
|
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||||
|
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||||
|
patent license to make, have made, use, offer to sell, sell, import,
|
||||||
|
transfer and otherwise run, modify and propagate the contents of this
|
||||||
|
implementation of Go, where such license applies only to those patent
|
||||||
|
claims, both currently owned or controlled by Google and acquired in
|
||||||
|
the future, licensable by Google that are necessarily infringed by this
|
||||||
|
implementation of Go. This grant does not include claims that would be
|
||||||
|
infringed only as a consequence of further modification of this
|
||||||
|
implementation. If you or your agent or exclusive licensee institute or
|
||||||
|
order or agree to the institution of patent litigation against any
|
||||||
|
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that this implementation of Go or any code incorporated within this
|
||||||
|
implementation of Go constitutes direct or contributory patent
|
||||||
|
infringement, or inducement of patent infringement, then any patent
|
||||||
|
rights granted to you under this License for this implementation of Go
|
||||||
|
shall terminate as of the date such litigation is filed.
|
||||||
144
vendor/golang.org/x/net/context/context.go
generated
vendored
Normal file
144
vendor/golang.org/x/net/context/context.go
generated
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2014 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 context defines the Context type, which carries deadlines,
|
||||||
|
// cancellation signals, and other request-scoped values across API boundaries
|
||||||
|
// and between processes.
|
||||||
|
// As of Go 1.7 this package is available in the standard library under the
|
||||||
|
// name [context], and migrating to it can be done automatically with [go fix].
|
||||||
|
//
|
||||||
|
// Incoming requests to a server should create a [Context], and outgoing
|
||||||
|
// calls to servers should accept a Context. The chain of function
|
||||||
|
// calls between them must propagate the Context, optionally replacing
|
||||||
|
// it with a derived Context created using [WithCancel], [WithDeadline],
|
||||||
|
// [WithTimeout], or [WithValue].
|
||||||
|
//
|
||||||
|
// Programs that use Contexts should follow these rules to keep interfaces
|
||||||
|
// consistent across packages and enable static analysis tools to check context
|
||||||
|
// propagation:
|
||||||
|
//
|
||||||
|
// Do not store Contexts inside a struct type; instead, pass a Context
|
||||||
|
// explicitly to each function that needs it. This is discussed further in
|
||||||
|
// https://go.dev/blog/context-and-structs. The Context should be the first
|
||||||
|
// parameter, typically named ctx:
|
||||||
|
//
|
||||||
|
// func DoSomething(ctx context.Context, arg Arg) error {
|
||||||
|
// // ... use ctx ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Do not pass a nil [Context], even if a function permits it. Pass [context.TODO]
|
||||||
|
// if you are unsure about which Context to use.
|
||||||
|
//
|
||||||
|
// Use context Values only for request-scoped data that transits processes and
|
||||||
|
// APIs, not for passing optional parameters to functions.
|
||||||
|
//
|
||||||
|
// The same Context may be passed to functions running in different goroutines;
|
||||||
|
// Contexts are safe for simultaneous use by multiple goroutines.
|
||||||
|
//
|
||||||
|
// See https://go.dev/blog/context for example code for a server that uses
|
||||||
|
// Contexts.
|
||||||
|
//
|
||||||
|
// [go fix]: https://go.dev/cmd/go#hdr-Update_packages_to_use_new_APIs
|
||||||
|
package context
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context" // standard library's context, as of Go 1.7
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Context carries a deadline, a cancellation signal, and other values across
|
||||||
|
// API boundaries.
|
||||||
|
//
|
||||||
|
// Context's methods may be called by multiple goroutines simultaneously.
|
||||||
|
type Context = context.Context
|
||||||
|
|
||||||
|
// Canceled is the error returned by [Context.Err] when the context is canceled
|
||||||
|
// for some reason other than its deadline passing.
|
||||||
|
var Canceled = context.Canceled
|
||||||
|
|
||||||
|
// DeadlineExceeded is the error returned by [Context.Err] when the context is canceled
|
||||||
|
// due to its deadline passing.
|
||||||
|
var DeadlineExceeded = context.DeadlineExceeded
|
||||||
|
|
||||||
|
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||||
|
// values, and has no deadline. It is typically used by the main function,
|
||||||
|
// initialization, and tests, and as the top-level Context for incoming
|
||||||
|
// requests.
|
||||||
|
func Background() Context {
|
||||||
|
return background
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||||
|
// it's unclear which Context to use or it is not yet available (because the
|
||||||
|
// surrounding function has not yet been extended to accept a Context
|
||||||
|
// parameter).
|
||||||
|
func TODO() Context {
|
||||||
|
return todo
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
background = context.Background()
|
||||||
|
todo = context.TODO()
|
||||||
|
)
|
||||||
|
|
||||||
|
// A CancelFunc tells an operation to abandon its work.
|
||||||
|
// A CancelFunc does not wait for the work to stop.
|
||||||
|
// A CancelFunc may be called by multiple goroutines simultaneously.
|
||||||
|
// After the first call, subsequent calls to a CancelFunc do nothing.
|
||||||
|
type CancelFunc = context.CancelFunc
|
||||||
|
|
||||||
|
// WithCancel returns a derived context that points to the parent context
|
||||||
|
// but has a new Done channel. The returned context's Done channel is closed
|
||||||
|
// when the returned cancel function is called or when the parent context's
|
||||||
|
// Done channel is closed, whichever happens first.
|
||||||
|
//
|
||||||
|
// Canceling this context releases resources associated with it, so code should
|
||||||
|
// call cancel as soon as the operations running in this [Context] complete.
|
||||||
|
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||||
|
return context.WithCancel(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDeadline returns a derived context that points to the parent context
|
||||||
|
// but has the deadline adjusted to be no later than d. If the parent's
|
||||||
|
// deadline is already earlier than d, WithDeadline(parent, d) is semantically
|
||||||
|
// equivalent to parent. The returned [Context.Done] channel is closed when
|
||||||
|
// the deadline expires, when the returned cancel function is called,
|
||||||
|
// or when the parent context's Done channel is closed, whichever happens first.
|
||||||
|
//
|
||||||
|
// Canceling this context releases resources associated with it, so code should
|
||||||
|
// call cancel as soon as the operations running in this [Context] complete.
|
||||||
|
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
|
||||||
|
return context.WithDeadline(parent, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||||
|
//
|
||||||
|
// Canceling this context releases resources associated with it, so code should
|
||||||
|
// call cancel as soon as the operations running in this [Context] complete:
|
||||||
|
//
|
||||||
|
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||||
|
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||||
|
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||||
|
// return slowOperation(ctx)
|
||||||
|
// }
|
||||||
|
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||||
|
return context.WithTimeout(parent, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithValue returns a derived context that points to the parent Context.
|
||||||
|
// In the derived context, the value associated with key is val.
|
||||||
|
//
|
||||||
|
// Use context Values only for request-scoped data that transits processes and
|
||||||
|
// APIs, not for passing optional parameters to functions.
|
||||||
|
//
|
||||||
|
// The provided key must be comparable and should not be of type
|
||||||
|
// string or any other built-in type to avoid collisions between
|
||||||
|
// packages using context. Users of WithValue should define their own
|
||||||
|
// types for keys. To avoid allocating when assigning to an
|
||||||
|
// interface{}, context keys often have concrete type
|
||||||
|
// struct{}. Alternatively, exported context key variables' static
|
||||||
|
// type should be a pointer or interface.
|
||||||
|
func WithValue(parent Context, key, val interface{}) Context {
|
||||||
|
return context.WithValue(parent, key, val)
|
||||||
|
}
|
||||||
4
vendor/golang.org/x/sys/LICENSE
generated
vendored
4
vendor/golang.org/x/sys/LICENSE
generated
vendored
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
Copyright 2009 The Go Authors.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
modification, are permitted provided that the following conditions are
|
modification, are permitted provided that the following conditions are
|
||||||
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
|
|||||||
copyright notice, this list of conditions and the following disclaimer
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
in the documentation and/or other materials provided with the
|
in the documentation and/or other materials provided with the
|
||||||
distribution.
|
distribution.
|
||||||
* Neither the name of Google Inc. nor the names of its
|
* Neither the name of Google LLC nor the names of its
|
||||||
contributors may be used to endorse or promote products derived from
|
contributors may be used to endorse or promote products derived from
|
||||||
this software without specific prior written permission.
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
|||||||
30
vendor/golang.org/x/sys/internal/unsafeheader/unsafeheader.go
generated
vendored
30
vendor/golang.org/x/sys/internal/unsafeheader/unsafeheader.go
generated
vendored
@@ -1,30 +0,0 @@
|
|||||||
// Copyright 2020 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 unsafeheader contains header declarations for the Go runtime's
|
|
||||||
// slice and string implementations.
|
|
||||||
//
|
|
||||||
// This package allows x/sys to use types equivalent to
|
|
||||||
// reflect.SliceHeader and reflect.StringHeader without introducing
|
|
||||||
// a dependency on the (relatively heavy) "reflect" package.
|
|
||||||
package unsafeheader
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Slice is the runtime representation of a slice.
|
|
||||||
// It cannot be used safely or portably and its representation may change in a later release.
|
|
||||||
type Slice struct {
|
|
||||||
Data unsafe.Pointer
|
|
||||||
Len int
|
|
||||||
Cap int
|
|
||||||
}
|
|
||||||
|
|
||||||
// String is the runtime representation of a string.
|
|
||||||
// It cannot be used safely or portably and its representation may change in a later release.
|
|
||||||
type String struct {
|
|
||||||
Data unsafe.Pointer
|
|
||||||
Len int
|
|
||||||
}
|
|
||||||
2
vendor/golang.org/x/sys/unix/README.md
generated
vendored
2
vendor/golang.org/x/sys/unix/README.md
generated
vendored
@@ -156,7 +156,7 @@ from the generated architecture-specific files listed below, and merge these
|
|||||||
into a common file for each OS.
|
into a common file for each OS.
|
||||||
|
|
||||||
The merge is performed in the following steps:
|
The merge is performed in the following steps:
|
||||||
1. Construct the set of common code that is idential in all architecture-specific files.
|
1. Construct the set of common code that is identical in all architecture-specific files.
|
||||||
2. Write this common code to the merged file.
|
2. Write this common code to the merged file.
|
||||||
3. Remove the common code from all architecture-specific files.
|
3. Remove the common code from all architecture-specific files.
|
||||||
|
|
||||||
|
|||||||
4
vendor/golang.org/x/sys/unix/aliases.go
generated
vendored
4
vendor/golang.org/x/sys/unix/aliases.go
generated
vendored
@@ -2,9 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos) && go1.9
|
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
|
||||||
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos
|
|
||||||
// +build go1.9
|
|
||||||
|
|
||||||
package unix
|
package unix
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/unix/asm_aix_ppc64.s
generated
vendored
1
vendor/golang.org/x/sys/unix/asm_aix_ppc64.s
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build gc
|
//go:build gc
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/sys/unix/asm_bsd_386.s
generated
vendored
2
vendor/golang.org/x/sys/unix/asm_bsd_386.s
generated
vendored
@@ -3,8 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (freebsd || netbsd || openbsd) && gc
|
//go:build (freebsd || netbsd || openbsd) && gc
|
||||||
// +build freebsd netbsd openbsd
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/sys/unix/asm_bsd_amd64.s
generated
vendored
2
vendor/golang.org/x/sys/unix/asm_bsd_amd64.s
generated
vendored
@@ -3,8 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (darwin || dragonfly || freebsd || netbsd || openbsd) && gc
|
//go:build (darwin || dragonfly || freebsd || netbsd || openbsd) && gc
|
||||||
// +build darwin dragonfly freebsd netbsd openbsd
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/sys/unix/asm_bsd_arm.s
generated
vendored
2
vendor/golang.org/x/sys/unix/asm_bsd_arm.s
generated
vendored
@@ -3,8 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (freebsd || netbsd || openbsd) && gc
|
//go:build (freebsd || netbsd || openbsd) && gc
|
||||||
// +build freebsd netbsd openbsd
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/sys/unix/asm_bsd_arm64.s
generated
vendored
2
vendor/golang.org/x/sys/unix/asm_bsd_arm64.s
generated
vendored
@@ -3,8 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
||||||
// +build darwin freebsd netbsd openbsd
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/sys/unix/asm_bsd_ppc64.s
generated
vendored
2
vendor/golang.org/x/sys/unix/asm_bsd_ppc64.s
generated
vendored
@@ -3,8 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
||||||
// +build darwin freebsd netbsd openbsd
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
2
vendor/golang.org/x/sys/unix/asm_bsd_riscv64.s
generated
vendored
2
vendor/golang.org/x/sys/unix/asm_bsd_riscv64.s
generated
vendored
@@ -3,8 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
//go:build (darwin || freebsd || netbsd || openbsd) && gc
|
||||||
// +build darwin freebsd netbsd openbsd
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/unix/asm_linux_386.s
generated
vendored
1
vendor/golang.org/x/sys/unix/asm_linux_386.s
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build gc
|
//go:build gc
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/unix/asm_linux_amd64.s
generated
vendored
1
vendor/golang.org/x/sys/unix/asm_linux_amd64.s
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build gc
|
//go:build gc
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
1
vendor/golang.org/x/sys/unix/asm_linux_arm.s
generated
vendored
1
vendor/golang.org/x/sys/unix/asm_linux_arm.s
generated
vendored
@@ -3,7 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build gc
|
//go:build gc
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
3
vendor/golang.org/x/sys/unix/asm_linux_arm64.s
generated
vendored
3
vendor/golang.org/x/sys/unix/asm_linux_arm64.s
generated
vendored
@@ -3,9 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build linux && arm64 && gc
|
//go:build linux && arm64 && gc
|
||||||
// +build linux
|
|
||||||
// +build arm64
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
3
vendor/golang.org/x/sys/unix/asm_linux_loong64.s
generated
vendored
3
vendor/golang.org/x/sys/unix/asm_linux_loong64.s
generated
vendored
@@ -3,9 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build linux && loong64 && gc
|
//go:build linux && loong64 && gc
|
||||||
// +build linux
|
|
||||||
// +build loong64
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
3
vendor/golang.org/x/sys/unix/asm_linux_mips64x.s
generated
vendored
3
vendor/golang.org/x/sys/unix/asm_linux_mips64x.s
generated
vendored
@@ -3,9 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build linux && (mips64 || mips64le) && gc
|
//go:build linux && (mips64 || mips64le) && gc
|
||||||
// +build linux
|
|
||||||
// +build mips64 mips64le
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
3
vendor/golang.org/x/sys/unix/asm_linux_mipsx.s
generated
vendored
3
vendor/golang.org/x/sys/unix/asm_linux_mipsx.s
generated
vendored
@@ -3,9 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build linux && (mips || mipsle) && gc
|
//go:build linux && (mips || mipsle) && gc
|
||||||
// +build linux
|
|
||||||
// +build mips mipsle
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
3
vendor/golang.org/x/sys/unix/asm_linux_ppc64x.s
generated
vendored
3
vendor/golang.org/x/sys/unix/asm_linux_ppc64x.s
generated
vendored
@@ -3,9 +3,6 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
//go:build linux && (ppc64 || ppc64le) && gc
|
//go:build linux && (ppc64 || ppc64le) && gc
|
||||||
// +build linux
|
|
||||||
// +build ppc64 ppc64le
|
|
||||||
// +build gc
|
|
||||||
|
|
||||||
#include "textflag.h"
|
#include "textflag.h"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user