Compare commits
33 Commits
c5f011dabc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c05a3b8ca7 | ||
|
|
de3db48d47 | ||
|
|
9ea444f365 | ||
|
|
3d85dbcb58 | ||
|
|
a128b473ff | ||
|
|
ee5d40cfee | ||
|
|
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 |
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
|
||||||
|
|||||||
758
.golangci.yml
Normal file
758
.golangci.yml
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
go: "1.25"
|
||||||
|
modules-download-mode: readonly
|
||||||
|
tests: false
|
||||||
|
allow-parallel-runners: true
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- asasalint
|
||||||
|
- asciicheck
|
||||||
|
- bodyclose
|
||||||
|
- containedctx
|
||||||
|
- contextcheck
|
||||||
|
- decorder
|
||||||
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
|
- errchkjson
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
|
- exptostd
|
||||||
|
- ginkgolinter
|
||||||
|
- gocheckcompilerdirectives
|
||||||
|
- gocritic
|
||||||
|
- godoclint
|
||||||
|
- godot
|
||||||
|
- godox
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- gosmopolitan
|
||||||
|
- govet
|
||||||
|
- grouper
|
||||||
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- intrange
|
||||||
|
- ireturn
|
||||||
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- misspell
|
||||||
|
- modernize
|
||||||
|
- nakedret
|
||||||
|
- nilerr
|
||||||
|
- nilnil
|
||||||
|
- nlreturn
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- nonamedreturns
|
||||||
|
- perfsprint
|
||||||
|
- predeclared
|
||||||
|
- promlinter
|
||||||
|
- reassign
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
|
- wastedassign
|
||||||
|
- wsl_v5
|
||||||
|
settings:
|
||||||
|
decorder:
|
||||||
|
dec-order:
|
||||||
|
- type
|
||||||
|
- const
|
||||||
|
- var
|
||||||
|
- func
|
||||||
|
gocritic:
|
||||||
|
disable-all: false
|
||||||
|
enable-all: true
|
||||||
|
disabled-checks:
|
||||||
|
- singleCaseSwitch
|
||||||
|
- octalLiteral
|
||||||
|
- regexpSimplify
|
||||||
|
- paramTypeCombine
|
||||||
|
- whyNoLint
|
||||||
|
- hugeParam
|
||||||
|
godoclint:
|
||||||
|
default: all
|
||||||
|
ignore-unexported: false
|
||||||
|
ignore-exported: false
|
||||||
|
include-unexported: true
|
||||||
|
disable:
|
||||||
|
- max-len
|
||||||
|
godot:
|
||||||
|
scope: all
|
||||||
|
exclude:
|
||||||
|
# Exclude todo and fixme comments.
|
||||||
|
- "^fixme:"
|
||||||
|
- "^todo:"
|
||||||
|
period: true
|
||||||
|
capital: false
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- FIXME
|
||||||
|
gosec: # go-fucking-sic
|
||||||
|
excludes:
|
||||||
|
# Боже, как же вы затрахали своей крипто-ебучкой, наркоманы хреновы!
|
||||||
|
- G405 # Detect the usage of DES or RC4
|
||||||
|
- G406 # Detect the usage of MD4 or RIPEMD160
|
||||||
|
# Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки
|
||||||
|
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
|
||||||
|
- G404 # Insecure random number source (rand)
|
||||||
|
- G501 # Import blocklist: crypto/md5
|
||||||
|
- G502 # Import blocklist: crypto/des
|
||||||
|
- G503 # Import blocklist: crypto/rc4
|
||||||
|
- G504 # Import blocklist: net/http/cgi
|
||||||
|
- G505 # Import blocklist: crypto/sha1
|
||||||
|
- G506 # Import blocklist: golang.org/x/crypto/md4
|
||||||
|
- G507 # Import blocklist: golang.org/x/crypto/ripemd160
|
||||||
|
config:
|
||||||
|
global:
|
||||||
|
audit: true
|
||||||
|
# Ваша сраная паранойя при создании файлов и каталогов мешает нормальной работе сервисов. Группы придуманы для
|
||||||
|
# удобства, группе удобно давать право чтения. Поэтому идите нахуй со своими ограничениями на чтение для
|
||||||
|
# группы, "безопасные" клоуны.
|
||||||
|
G301: "0755"
|
||||||
|
G306: "0644"
|
||||||
|
govet:
|
||||||
|
disable-all: false
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- shadow
|
||||||
|
- fieldalignment
|
||||||
|
grouper:
|
||||||
|
# Require the use of a single global 'const' declaration only.
|
||||||
|
# Default: false
|
||||||
|
const-require-single-const: true
|
||||||
|
# Require the use of grouped global 'const' declarations.
|
||||||
|
# Default: false
|
||||||
|
const-require-grouping: true
|
||||||
|
# Require the use of a single 'import' declaration only.
|
||||||
|
# Default: false
|
||||||
|
import-require-single-import: true
|
||||||
|
# Require the use of grouped 'import' declarations.
|
||||||
|
# Default: false
|
||||||
|
import-require-grouping: true
|
||||||
|
# Require the use of a single global 'type' declaration only.
|
||||||
|
# Default: false
|
||||||
|
type-require-single-type: true
|
||||||
|
# Require the use of grouped global 'type' declarations.
|
||||||
|
# Default: false
|
||||||
|
type-require-grouping: true
|
||||||
|
# Require the use of a single global 'var' declaration only.
|
||||||
|
# Default: false
|
||||||
|
var-require-single-var: true
|
||||||
|
# Require the use of grouped global 'var' declarations.
|
||||||
|
# Default: false
|
||||||
|
var-require-grouping: true
|
||||||
|
misspell:
|
||||||
|
# Correct spellings using locale preferences for US or UK.
|
||||||
|
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
|
||||||
|
# Default is to use a neutral variety of English.
|
||||||
|
locale: US
|
||||||
|
# Typos to ignore.
|
||||||
|
# Should be in lower case.
|
||||||
|
# Default: []
|
||||||
|
ignore-rules:
|
||||||
|
- someword
|
||||||
|
# Extra word corrections.
|
||||||
|
# `typo` and `correction` should only contain letters.
|
||||||
|
# The words are case-insensitive.
|
||||||
|
# Default: []
|
||||||
|
extra-words:
|
||||||
|
- typo: "iff"
|
||||||
|
correction: "if"
|
||||||
|
- typo: "cancelation"
|
||||||
|
correction: "cancellation"
|
||||||
|
# Mode of the analysis:
|
||||||
|
# - default: checks all the file content.
|
||||||
|
# - restricted: checks only comments.
|
||||||
|
# Default: ""
|
||||||
|
mode: restricted
|
||||||
|
nonamedreturns:
|
||||||
|
# Report named error if it is assigned inside defer.
|
||||||
|
# Default: false
|
||||||
|
report-error-in-defer: true
|
||||||
|
perfsprint:
|
||||||
|
integer-format: true
|
||||||
|
int-conversion: true
|
||||||
|
error-format: true
|
||||||
|
err-error: false
|
||||||
|
errorf: true
|
||||||
|
string-format: true
|
||||||
|
sprintf1: true
|
||||||
|
strconcat: true
|
||||||
|
bool-format: true
|
||||||
|
hex-format: true
|
||||||
|
concat-loop: true
|
||||||
|
loop-other-ops: false
|
||||||
|
revive:
|
||||||
|
enable-all-rules: false
|
||||||
|
staticcheck:
|
||||||
|
# https://staticcheck.dev/docs/configuration/options/#dot_import_whitelist
|
||||||
|
# Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"]
|
||||||
|
dot-import-whitelist:
|
||||||
|
- fmt
|
||||||
|
# https://staticcheck.dev/docs/configuration/options/#initialisms
|
||||||
|
# Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"]
|
||||||
|
initialisms: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"]
|
||||||
|
# https://staticcheck.dev/docs/configuration/options/#http_status_code_whitelist
|
||||||
|
# Default: ["200", "400", "404", "500"]
|
||||||
|
http-status-code-whitelist: ["200", "400", "404", "500"]
|
||||||
|
# SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks
|
||||||
|
# Example (to disable some checks): [ "all", "-SA1000", "-SA1001"]
|
||||||
|
# Run `GL_DEBUG=staticcheck golangci-lint run --enable=staticcheck` to see all available checks and enabled by config checks.
|
||||||
|
# Default: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
|
||||||
|
checks:
|
||||||
|
# Invalid regular expression.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1000
|
||||||
|
- SA1000
|
||||||
|
# Invalid template.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1001
|
||||||
|
- SA1001
|
||||||
|
# Invalid format in 'time.Parse'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1002
|
||||||
|
- SA1002
|
||||||
|
# Unsupported argument to functions in 'encoding/binary'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1003
|
||||||
|
- SA1003
|
||||||
|
# Suspiciously small untyped constant in 'time.Sleep'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1004
|
||||||
|
- SA1004
|
||||||
|
# Invalid first argument to 'exec.Command'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1005
|
||||||
|
- SA1005
|
||||||
|
# 'Printf' with dynamic first argument and no further arguments.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1006
|
||||||
|
- SA1006
|
||||||
|
# Invalid URL in 'net/url.Parse'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1007
|
||||||
|
- SA1007
|
||||||
|
# Non-canonical key in 'http.Header' map.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1008
|
||||||
|
- SA1008
|
||||||
|
# '(*regexp.Regexp).FindAll' called with 'n == 0', which will always return zero results.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1010
|
||||||
|
- SA1010
|
||||||
|
# Various methods in the "strings" package expect valid UTF-8, but invalid input is provided.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1011
|
||||||
|
- SA1011
|
||||||
|
# A nil 'context.Context' is being passed to a function, consider using 'context.TODO' instead.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1012
|
||||||
|
- SA1012
|
||||||
|
# 'io.Seeker.Seek' is being called with the whence constant as the first argument, but it should be the second.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1013
|
||||||
|
- SA1013
|
||||||
|
# Non-pointer value passed to 'Unmarshal' or 'Decode'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1014
|
||||||
|
- SA1014
|
||||||
|
# Using 'time.Tick' in a way that will leak. Consider using 'time.NewTicker', and only use 'time.Tick' in tests, commands and endless functions.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1015
|
||||||
|
- SA1015
|
||||||
|
# Trapping a signal that cannot be trapped.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1016
|
||||||
|
- SA1016
|
||||||
|
# Channels used with 'os/signal.Notify' should be buffered.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1017
|
||||||
|
- SA1017
|
||||||
|
# 'strings.Replace' called with 'n == 0', which does nothing.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1018
|
||||||
|
- SA1018
|
||||||
|
# Using a deprecated function, variable, constant or field.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1019
|
||||||
|
- SA1019
|
||||||
|
# Using an invalid host:port pair with a 'net.Listen'-related function.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1020
|
||||||
|
- SA1020
|
||||||
|
# Using 'bytes.Equal' to compare two 'net.IP'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1021
|
||||||
|
- SA1021
|
||||||
|
# Modifying the buffer in an 'io.Writer' implementation.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1023
|
||||||
|
- SA1023
|
||||||
|
# A string cutset contains duplicate characters.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1024
|
||||||
|
- SA1024
|
||||||
|
# It is not possible to use '(*time.Timer).Reset''s return value correctly.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1025
|
||||||
|
- SA1025
|
||||||
|
# Cannot marshal channels or functions.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1026
|
||||||
|
- SA1026
|
||||||
|
# Atomic access to 64-bit variable must be 64-bit aligned.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1027
|
||||||
|
- SA1027
|
||||||
|
# 'sort.Slice' can only be used on slices.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1028
|
||||||
|
- SA1028
|
||||||
|
# Inappropriate key in call to 'context.WithValue'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1029
|
||||||
|
- SA1029
|
||||||
|
# Invalid argument in call to a 'strconv' function.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1030
|
||||||
|
- SA1030
|
||||||
|
# Overlapping byte slices passed to an encoder.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1031
|
||||||
|
- SA1031
|
||||||
|
# Wrong order of arguments to 'errors.Is'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA1032
|
||||||
|
- SA1032
|
||||||
|
# 'sync.WaitGroup.Add' called inside the goroutine, leading to a race condition.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA2000
|
||||||
|
- SA2000
|
||||||
|
# Empty critical section, did you mean to defer the unlock?.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA2001
|
||||||
|
- SA2001
|
||||||
|
# Called 'testing.T.FailNow' or 'SkipNow' in a goroutine, which isn't allowed.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA2002
|
||||||
|
- SA2002
|
||||||
|
# Deferred 'Lock' right after locking, likely meant to defer 'Unlock' instead.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA2003
|
||||||
|
- SA2003
|
||||||
|
# 'TestMain' doesn't call 'os.Exit', hiding test failures.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA3000
|
||||||
|
- SA3000
|
||||||
|
# Assigning to 'b.N' in benchmarks distorts the results.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA3001
|
||||||
|
- SA3001
|
||||||
|
# Binary operator has identical expressions on both sides.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4000
|
||||||
|
- SA4000
|
||||||
|
# '&*x' gets simplified to 'x', it does not copy 'x'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4001
|
||||||
|
- SA4001
|
||||||
|
# Comparing unsigned values against negative values is pointless.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4003
|
||||||
|
- SA4003
|
||||||
|
# The loop exits unconditionally after one iteration.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4004
|
||||||
|
- SA4004
|
||||||
|
# Field assignment that will never be observed. Did you mean to use a pointer receiver?.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4005
|
||||||
|
- SA4005
|
||||||
|
# A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4006
|
||||||
|
- SA4006
|
||||||
|
# The variable in the loop condition never changes, are you incrementing the wrong variable?.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4008
|
||||||
|
- SA4008
|
||||||
|
# A function argument is overwritten before its first use.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4009
|
||||||
|
- SA4009
|
||||||
|
# The result of 'append' will never be observed anywhere.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4010
|
||||||
|
- SA4010
|
||||||
|
# Break statement with no effect. Did you mean to break out of an outer loop?.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4011
|
||||||
|
- SA4011
|
||||||
|
# Comparing a value against NaN even though no value is equal to NaN.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4012
|
||||||
|
- SA4012
|
||||||
|
# Negating a boolean twice ('!!b') is the same as writing 'b'. This is either redundant, or a typo.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4013
|
||||||
|
- SA4013
|
||||||
|
# An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4014
|
||||||
|
- SA4014
|
||||||
|
# Calling functions like 'math.Ceil' on floats converted from integers doesn't do anything useful.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4015
|
||||||
|
- SA4015
|
||||||
|
# Certain bitwise operations, such as 'x ^ 0', do not do anything useful.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4016
|
||||||
|
- SA4016
|
||||||
|
# Discarding the return values of a function without side effects, making the call pointless.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4017
|
||||||
|
- SA4017
|
||||||
|
# Self-assignment of variables.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4018
|
||||||
|
- SA4018
|
||||||
|
# Multiple, identical build constraints in the same file.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4019
|
||||||
|
- SA4019
|
||||||
|
# Unreachable case clause in a type switch.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4020
|
||||||
|
- SA4020
|
||||||
|
# "x = append(y)" is equivalent to "x = y".
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4021
|
||||||
|
- SA4021
|
||||||
|
# Comparing the address of a variable against nil.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4022
|
||||||
|
- SA4022
|
||||||
|
# Impossible comparison of interface value with untyped nil.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4023
|
||||||
|
- SA4023
|
||||||
|
# Checking for impossible return value from a builtin function.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4024
|
||||||
|
- SA4024
|
||||||
|
# Integer division of literals that results in zero.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4025
|
||||||
|
- SA4025
|
||||||
|
# Go constants cannot express negative zero.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4026
|
||||||
|
- SA4026
|
||||||
|
# '(*net/url.URL).Query' returns a copy, modifying it doesn't change the URL.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4027
|
||||||
|
- SA4027
|
||||||
|
# 'x % 1' is always zero.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4028
|
||||||
|
- SA4028
|
||||||
|
# Ineffective attempt at sorting slice.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4029
|
||||||
|
- SA4029
|
||||||
|
# Ineffective attempt at generating random number.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4030
|
||||||
|
- SA4030
|
||||||
|
# Checking never-nil value against nil.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4031
|
||||||
|
- SA4031
|
||||||
|
# Comparing 'runtime.GOOS' or 'runtime.GOARCH' against impossible value.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA4032
|
||||||
|
- SA4032
|
||||||
|
# Assignment to nil map.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5000
|
||||||
|
- SA5000
|
||||||
|
# Deferring 'Close' before checking for a possible error.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5001
|
||||||
|
- SA5001
|
||||||
|
# The empty for loop ("for {}") spins and can block the scheduler.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5002
|
||||||
|
- SA5002
|
||||||
|
# Defers in infinite loops will never execute.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5003
|
||||||
|
- SA5003
|
||||||
|
# "for { select { ..." with an empty default branch spins.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5004
|
||||||
|
- SA5004
|
||||||
|
# The finalizer references the finalized object, preventing garbage collection.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5005
|
||||||
|
- SA5005
|
||||||
|
# Infinite recursive call.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5007
|
||||||
|
- SA5007
|
||||||
|
# Invalid struct tag.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5008
|
||||||
|
- SA5008
|
||||||
|
# Invalid Printf call.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5009
|
||||||
|
- SA5009
|
||||||
|
# Impossible type assertion.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5010
|
||||||
|
- SA5010
|
||||||
|
# Possible nil pointer dereference.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5011
|
||||||
|
- SA5011
|
||||||
|
# Passing odd-sized slice to function expecting even size.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA5012
|
||||||
|
- SA5012
|
||||||
|
# Using 'regexp.Match' or related in a loop, should use 'regexp.Compile'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA6000
|
||||||
|
- SA6000
|
||||||
|
# Missing an optimization opportunity when indexing maps by byte slices.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA6001
|
||||||
|
- SA6001
|
||||||
|
# Storing non-pointer values in 'sync.Pool' allocates memory.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA6002
|
||||||
|
- SA6002
|
||||||
|
# Converting a string to a slice of runes before ranging over it.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA6003
|
||||||
|
- SA6003
|
||||||
|
# Inefficient string comparison with 'strings.ToLower' or 'strings.ToUpper'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA6005
|
||||||
|
- SA6005
|
||||||
|
# Using io.WriteString to write '[]byte'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA6006
|
||||||
|
- SA6006
|
||||||
|
# Defers in range loops may not run when you expect them to.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9001
|
||||||
|
- SA9001
|
||||||
|
# Using a non-octal 'os.FileMode' that looks like it was meant to be in octal.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9002
|
||||||
|
- SA9002
|
||||||
|
# Empty body in an if or else branch.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9003
|
||||||
|
- SA9003
|
||||||
|
# Only the first constant has an explicit type.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9004
|
||||||
|
- SA9004
|
||||||
|
# Trying to marshal a struct with no public fields nor custom marshaling.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9005
|
||||||
|
- SA9005
|
||||||
|
# Dubious bit shifting of a fixed size integer value.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9006
|
||||||
|
- SA9006
|
||||||
|
# Deleting a directory that shouldn't be deleted.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9007
|
||||||
|
- SA9007
|
||||||
|
# 'else' branch of a type assertion is probably not reading the right value.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9008
|
||||||
|
- SA9008
|
||||||
|
# Ineffectual Go compiler directive.
|
||||||
|
# https://staticcheck.dev/docs/checks/#SA9009
|
||||||
|
- SA9009
|
||||||
|
# Incorrect or missing package comment.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1000
|
||||||
|
- ST1000
|
||||||
|
# Dot imports are discouraged.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1001
|
||||||
|
- ST1001
|
||||||
|
# Poorly chosen identifier.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1003
|
||||||
|
- ST1003
|
||||||
|
# Incorrectly formatted error string.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1005
|
||||||
|
- ST1005
|
||||||
|
# Poorly chosen receiver name.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1006
|
||||||
|
- ST1006
|
||||||
|
# A function's error value should be its last return value.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1008
|
||||||
|
- ST1008
|
||||||
|
# Poorly chosen name for variable of type 'time.Duration'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1011
|
||||||
|
- ST1011
|
||||||
|
# Poorly chosen name for error variable.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1012
|
||||||
|
- ST1012
|
||||||
|
# Should use constants for HTTP error codes, not magic numbers.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1013
|
||||||
|
- ST1013
|
||||||
|
# A switch's default case should be the first or last case.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1015
|
||||||
|
- ST1015
|
||||||
|
# Use consistent method receiver names.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1016
|
||||||
|
- ST1016
|
||||||
|
# Don't use Yoda conditions.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1017
|
||||||
|
- ST1017
|
||||||
|
# Avoid zero-width and control characters in string literals.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1018
|
||||||
|
- ST1018
|
||||||
|
# Importing the same package multiple times.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1019
|
||||||
|
- ST1019
|
||||||
|
# The documentation of an exported function should start with the function's name.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1020
|
||||||
|
- ST1020
|
||||||
|
# The documentation of an exported type should start with type's name.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1021
|
||||||
|
- ST1021
|
||||||
|
# The documentation of an exported variable or constant should start with variable's name.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1022
|
||||||
|
- ST1022
|
||||||
|
# Redundant type in variable declaration.
|
||||||
|
# https://staticcheck.dev/docs/checks/#ST1023
|
||||||
|
- ST1023
|
||||||
|
# Use plain channel send or receive instead of single-case select.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1000
|
||||||
|
- S1000
|
||||||
|
# Replace for loop with call to copy.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1001
|
||||||
|
- S1001
|
||||||
|
# Omit comparison with boolean constant.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1002
|
||||||
|
- S1002
|
||||||
|
# Replace call to 'strings.Index' with 'strings.Contains'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1003
|
||||||
|
- S1003
|
||||||
|
# Replace call to 'bytes.Compare' with 'bytes.Equal'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1004
|
||||||
|
- S1004
|
||||||
|
# Drop unnecessary use of the blank identifier.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1005
|
||||||
|
- S1005
|
||||||
|
# Use "for { ... }" for infinite loops.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1006
|
||||||
|
- S1006
|
||||||
|
# Simplify regular expression by using raw string literal.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1007
|
||||||
|
- S1007
|
||||||
|
# Simplify returning boolean expression.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1008
|
||||||
|
- S1008
|
||||||
|
# Omit redundant nil check on slices, maps, and channels.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1009
|
||||||
|
- S1009
|
||||||
|
# Omit default slice index.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1010
|
||||||
|
- S1010
|
||||||
|
# Use a single 'append' to concatenate two slices.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1011
|
||||||
|
- S1011
|
||||||
|
# Replace 'time.Now().Sub(x)' with 'time.Since(x)'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1012
|
||||||
|
- S1012
|
||||||
|
# Use a type conversion instead of manually copying struct fields.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1016
|
||||||
|
- S1016
|
||||||
|
# Replace manual trimming with 'strings.TrimPrefix'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1017
|
||||||
|
- S1017
|
||||||
|
# Use "copy" for sliding elements.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1018
|
||||||
|
- S1018
|
||||||
|
# Simplify "make" call by omitting redundant arguments.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1019
|
||||||
|
- S1019
|
||||||
|
# Omit redundant nil check in type assertion.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1020
|
||||||
|
- S1020
|
||||||
|
# Merge variable declaration and assignment.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1021
|
||||||
|
- S1021
|
||||||
|
# Omit redundant control flow.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1023
|
||||||
|
- S1023
|
||||||
|
# Replace 'x.Sub(time.Now())' with 'time.Until(x)'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1024
|
||||||
|
- S1024
|
||||||
|
# Don't use 'fmt.Sprintf("%s", x)' unnecessarily.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1025
|
||||||
|
- S1025
|
||||||
|
# Simplify error construction with 'fmt.Errorf'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1028
|
||||||
|
- S1028
|
||||||
|
# Range over the string directly.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1029
|
||||||
|
- S1029
|
||||||
|
# Use 'bytes.Buffer.String' or 'bytes.Buffer.Bytes'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1030
|
||||||
|
- S1030
|
||||||
|
# Omit redundant nil check around loop.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1031
|
||||||
|
- S1031
|
||||||
|
# Use 'sort.Ints(x)', 'sort.Float64s(x)', and 'sort.Strings(x)'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1032
|
||||||
|
- S1032
|
||||||
|
# Unnecessary guard around call to "delete".
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1033
|
||||||
|
- S1033
|
||||||
|
# Use result of type assertion to simplify cases.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1034
|
||||||
|
- S1034
|
||||||
|
# Redundant call to 'net/http.CanonicalHeaderKey' in method call on 'net/http.Header'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1035
|
||||||
|
- S1035
|
||||||
|
# Unnecessary guard around map access.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1036
|
||||||
|
- S1036
|
||||||
|
# Elaborate way of sleeping.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1037
|
||||||
|
- S1037
|
||||||
|
# Unnecessarily complex way of printing formatted string.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1038
|
||||||
|
- S1038
|
||||||
|
# Unnecessary use of 'fmt.Sprint'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1039
|
||||||
|
- S1039
|
||||||
|
# Type assertion to current type.
|
||||||
|
# https://staticcheck.dev/docs/checks/#S1040
|
||||||
|
- S1040
|
||||||
|
# Apply De Morgan's law.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1001
|
||||||
|
- QF1001
|
||||||
|
# Convert untagged switch to tagged switch.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1002
|
||||||
|
- QF1002
|
||||||
|
# Convert if/else-if chain to tagged switch.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1003
|
||||||
|
- QF1003
|
||||||
|
# Use 'strings.ReplaceAll' instead of 'strings.Replace' with 'n == -1'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1004
|
||||||
|
- QF1004
|
||||||
|
# Expand call to 'math.Pow'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1005
|
||||||
|
- QF1005
|
||||||
|
# Lift 'if'+'break' into loop condition.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1006
|
||||||
|
- QF1006
|
||||||
|
# Merge conditional assignment into variable declaration.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1007
|
||||||
|
- QF1007
|
||||||
|
# Omit embedded fields from selector expression.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1008
|
||||||
|
- QF1008
|
||||||
|
# Use 'time.Time.Equal' instead of '==' operator.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1009
|
||||||
|
- QF1009
|
||||||
|
# Convert slice of bytes to string when printing it.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1010
|
||||||
|
- QF1010
|
||||||
|
# Omit redundant type from variable declaration.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1011
|
||||||
|
- QF1011
|
||||||
|
# Use 'fmt.Fprintf(x, ...)' instead of 'x.Write(fmt.Sprintf(...))'.
|
||||||
|
# https://staticcheck.dev/docs/checks/#QF1012
|
||||||
|
- QF1012
|
||||||
|
unconvert:
|
||||||
|
# Remove conversions that force intermediate rounding.
|
||||||
|
# Default: false
|
||||||
|
fast-math: true
|
||||||
|
# Be more conservative (experimental).
|
||||||
|
# Default: false
|
||||||
|
safe: true
|
||||||
|
usestdlibvars:
|
||||||
|
# Suggest the use of http.MethodXX.
|
||||||
|
# Default: true
|
||||||
|
http-method: false
|
||||||
|
# Suggest the use of http.StatusXX.
|
||||||
|
# Default: true
|
||||||
|
http-status-code: false
|
||||||
|
# Suggest the use of time.Month in time.Date.
|
||||||
|
# Default: false
|
||||||
|
time-date-month: true
|
||||||
|
# Suggest the use of time.Weekday.String().
|
||||||
|
# Default: true
|
||||||
|
time-weekday: true
|
||||||
|
# Suggest the use of time.Month.String().
|
||||||
|
# Default: false
|
||||||
|
time-month: true
|
||||||
|
# Suggest the use of time.Layout.
|
||||||
|
# Default: false
|
||||||
|
time-layout: true
|
||||||
|
# Suggest the use of crypto.Hash.String().
|
||||||
|
# Default: false
|
||||||
|
crypto-hash: true
|
||||||
|
# Suggest the use of rpc.DefaultXXPath.
|
||||||
|
# Default: false
|
||||||
|
default-rpc-path: true
|
||||||
|
# Suggest the use of sql.LevelXX.String().
|
||||||
|
# Default: false
|
||||||
|
sql-isolation-level: true
|
||||||
|
# Suggest the use of tls.SignatureScheme.String().
|
||||||
|
# Default: false
|
||||||
|
tls-signature-scheme: true
|
||||||
|
# Suggest the use of constant.Kind.String().
|
||||||
|
# Default: false
|
||||||
|
constant-kind: true
|
||||||
|
wsl_v5:
|
||||||
|
allow-first-in-block: true
|
||||||
|
allow-whole-block: true
|
||||||
|
branch-max-lines: 2
|
||||||
|
case-max-lines: 2
|
||||||
|
default: all
|
||||||
|
disable:
|
||||||
|
- if
|
||||||
|
- err
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
18
Design.txt
18
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)
|
||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
@@ -67,3 +60,14 @@ Cуществует несколько видов пинга:
|
|||||||
------------------------------------------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
Бот не читает историю, когда присоединяется к чатам, поэтому если его какое-то время не было в комнате и кто-то произнёс
|
Бот не читает историю, когда присоединяется к чатам, поэтому если его какое-то время не было в комнате и кто-то произнёс
|
||||||
непотребную фразу, ретроспективного бана не будет.
|
непотребную фразу, ретроспективного бана не будет.
|
||||||
|
|
||||||
|
|
||||||
|
Проблема реконнекта
|
||||||
|
------------------------------------------------------------------------------------------------------------------------
|
||||||
|
На текущий момент решена не совсем чисто. Скорее всего, при реконнекте утекают ресурсы, связанные с jabber-соединением.
|
||||||
|
Проблема в том, что приходится пересоздавать основной "объект" со всеми его полями.
|
||||||
|
|
||||||
|
Кроме того, sig handler фактичеки не завершается, потому что в него не приходит ни одного сигнала и формально остаётся
|
||||||
|
висеть на channel-е навечно.
|
||||||
|
|
||||||
|
А ещё есть пингеры каналов и пингеры сервера. Их бы тоже завершать явным образом.
|
||||||
|
|||||||
43
Makefile
43
Makefile
@@ -1,11 +1,11 @@
|
|||||||
#!/usr/bin/env gmake -f
|
#!/usr/bin/env gmake -f
|
||||||
|
|
||||||
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -pgo=auto
|
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
||||||
FILELIST=collection.go types.go globals.go lib.go event_parser.go buny.go commands.go main.go
|
MYNAME=buny-jabber-bot
|
||||||
|
BINARY=$(MYNAME)
|
||||||
|
TEST1=xml-software-version-query
|
||||||
|
|
||||||
BINARY=buny-jabber-bot
|
# На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe
|
||||||
|
|
||||||
# На windows имя бинарника может зависеть не только от платформы, но и от выранной цели, для linux-а суффикс .exe
|
|
||||||
# не нужен
|
# не нужен
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
ifeq ($(strip $(GOOS)),)
|
ifeq ($(strip $(GOOS)),)
|
||||||
@@ -23,34 +23,34 @@ define IFS
|
|||||||
|
|
||||||
endef
|
endef
|
||||||
|
|
||||||
# Используем классические таргеты, где первый встречаемый является таргетом по-умолчанию
|
## Используем классические таргеты, где первый встречаемый является таргетом по-умолчанию
|
||||||
all: clean build
|
all: clean build
|
||||||
|
|
||||||
|
## Этот таргет собирает бинарь
|
||||||
build:
|
build:
|
||||||
# Ну и дальше просто билдим бинарник средствами гошки
|
# Ну и дальше просто билдим бинарник средствами гошки
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
# вариант с powershell на windows
|
# вариант с powershell на windows
|
||||||
ifeq ($(SHELL),sh.exe)
|
ifeq ($(SHELL),sh.exe)
|
||||||
SET CGO_ENABLED=0
|
SET CGO_ENABLED=0
|
||||||
go build ${BUILDOPTS} -o ${BINARY} ${FILELIST}
|
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
else
|
else
|
||||||
# вариант с jetbrains golang на windows
|
# вариант с jetbrains golang на windows
|
||||||
CGO_ENABLED=0
|
CGO_ENABLED=0
|
||||||
go build ${BUILDOPTS} -o ${BINARY} ${FILELIST}
|
go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
endif
|
endif
|
||||||
# вариант с bash/git (windows) и bash (linux)
|
# вариант с bash/git (windows) и bash (linux)
|
||||||
else
|
else
|
||||||
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ${FILELIST}
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY} ./cmd/${MYNAME}
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
# Удаляем бинарник средствами go
|
## Удаляем бинарник средствами go
|
||||||
clean:
|
clean:
|
||||||
go clean
|
$(RM) ${BINARY}
|
||||||
|
|
||||||
|
|
||||||
# Служебный таргет, для целей разработки. Обновляет завендоренные либы, брутальным образом.
|
## Служебный таргет, для целей разработки. Обновляет завендоренные либы, брутальным образом.
|
||||||
upgrade:
|
upgrade:
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
# вариант с jetbrains golang на windows или powershell на windows
|
# вариант с jetbrains golang на windows или powershell на windows
|
||||||
@@ -68,4 +68,21 @@ endif
|
|||||||
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:
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,20 +1,31 @@
|
|||||||
# 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-е злодея
|
||||||
регулярными выражениями в nick-е или jid-е злодея, а также по регулярным выражениям характерных фраз. Правила можно
|
- По регулярным выражениям характерных фраз.
|
||||||
настроить как глобально, для всех комнат, где присутсвует бот, так и для каждой комнаты отдельно. При изменении списка
|
- По названию и/или версии и, если есть, ос злодея.
|
||||||
правил бота надо перезапускать.
|
- Правила можно настроить как глобально, для всех комнат, где присутствует бот, так и для каждой комнаты отдельно.
|
||||||
|
- При изменении списка правил бота не надо перезапускать, достаточно отдать ему команду rehash либо в приват, либо
|
||||||
|
прям в чатике.
|
||||||
|
* Есть настройка заходить в разные комнаты под разными никами.
|
||||||
|
|
||||||
## Что он не может?
|
## Что он не может?
|
||||||
|
|
||||||
@@ -46,7 +57,7 @@ make
|
|||||||
|
|
||||||
Почему нельзя оставить комнату анонимной? А тут всё просто, в банлист заносится реальный jid пользователя, а не его ник.
|
Почему нельзя оставить комнату анонимной? А тут всё просто, в банлист заносится реальный jid пользователя, а не его ник.
|
||||||
Соответственно, бот должен видеть этот самый реальный jid. В анонимной комнате бот не сможет увидеть реальный jid.
|
Соответственно, бот должен видеть этот самый реальный jid. В анонимной комнате бот не сможет увидеть реальный jid.
|
||||||
А автокик - это конечно хорошо, но есть нюанс: можно, например, гадить ником.
|
А автокик - это, конечно, хорошо, но есть нюанс: можно, например, гадить ником, поэтому бот в автокик не умеет.
|
||||||
|
|
||||||
## Как работает бан?
|
## Как работает бан?
|
||||||
|
|
||||||
|
|||||||
BIN
assets/avatar.png
Normal file
BIN
assets/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
370
buny.go
370
buny.go
@@ -1,370 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// bunyPresense производит проверку по бело-чёрным спискам. Если presence пришёл от злодея (из чёрного списка), то
|
|
||||||
// отправляет его в бан.
|
|
||||||
func bunyPresense(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 {
|
|
||||||
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", evilJid, jidRegexp)
|
|
||||||
|
|
||||||
if re.MatchString(evilJid) {
|
|
||||||
if id, err := squash(room, evilJid, room+"/"+evilNick, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// Баним именно jid
|
|
||||||
if id, err := squash(room, evilJid, room+"/"+evilNick, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
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", evilJid, room, jidRegexp)
|
|
||||||
|
|
||||||
if re.MatchString(evilJid) {
|
|
||||||
if id, err := squash(room, evilJid, room+"/"+evilNick, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// Баним именно jid
|
|
||||||
if id, err := squash(room, evilJid, room+"/"+evilNick, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// bunyChat проиводит проверку сообщений участников чата по списку забаненных фраз и в случае нахождения запрещённого
|
|
||||||
// шаблона банит участника чата.
|
|
||||||
func 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 roomsConnected {
|
|
||||||
if cRoom == room {
|
|
||||||
for _, bEntry := range 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) {
|
|
||||||
if id, err := squash(room, getRealJIDfromNick(v.Remote), v.Remote, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if id, err := squash(room, getRealJIDfromNick(v.Remote), v.Remote, bEntry.ReasonEnable, v.Type); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to ban user: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// squash банит указанный jid в указанной комнате.
|
|
||||||
// reasonEnable указывает, надо ли писать дату автобана в банлисте в поле reason (это единственная причина, в которую
|
|
||||||
// умеет бот).
|
|
||||||
func squash(room, jid string, nick string, reasonEnable bool, vType string) (string, error) {
|
|
||||||
var (
|
|
||||||
id string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Infof("Hammer falls on %s(%s)", nick, 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>"
|
|
||||||
|
|
||||||
// Выжидаем некоторое время перед баном. А то можно настолько рано забанить, что сервер не внесёт злодея в банлист
|
|
||||||
// комнаты и пришлёт affiliation: none вместо affiliation: outcast.
|
|
||||||
if config.Jabber.BanDelay > 0 {
|
|
||||||
time.Sleep(time.Duration(config.Jabber.BanDelay) * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
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: */
|
|
||||||
149
cmd/buny-jabber-bot/main.go
Normal file
149
cmd/buny-jabber-bot/main.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// Package main содержит основную функцию для запуска логики бота.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"buny-jabber-bot/internal/jabber"
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
"gopkg.in/tomb.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed version
|
||||||
|
var version string //nolint: grouper
|
||||||
|
|
||||||
|
// main - фактичеcки, начало и основное тело программы.
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
logfile = os.Stderr
|
||||||
|
j = jabber.Jabber{ //nolint:exhaustruct
|
||||||
|
SigChan: make(chan os.Signal, 1),
|
||||||
|
GTomb: tomb.Tomb{},
|
||||||
|
RoomsConnected: make([]string, 1),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := j.ReadConfig(); err != nil {
|
||||||
|
log.Errorf("%s", err)
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.C.Version = version
|
||||||
|
|
||||||
|
// Откроем лог и скормим его логгеру.
|
||||||
|
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.Errorf("Unable to open log file %s: %s", j.C.Log, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Init(j.C.Loglevel, logfile)
|
||||||
|
|
||||||
|
j.C.ExeName, err = os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("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 log.LogLevel == slog.LevelDebug {
|
||||||
|
if j.C.Debug.Lib {
|
||||||
|
verboseClient = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Самое время вгрузить белые и чёрные списки, если что - получим ошибку в лог.
|
||||||
|
if err := j.ReadWhitelist(); err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := j.ReadBlacklist(); err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хэндлер сигналов.
|
||||||
|
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,
|
||||||
|
DebugWriter: log.Writer,
|
||||||
|
PeriodicServerPings: false,
|
||||||
|
ReportSoftwareVersion: true,
|
||||||
|
SoftwareName: "Buny Jabber Bot",
|
||||||
|
SoftwareVersion: version,
|
||||||
|
ReportSoftwareOS: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Враппер основной программы. Фактически на каждой серьёзной ошибке, например, сетевой, 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
|
||||||
@@ -18,3 +18,6 @@ start_pre() {
|
|||||||
mkdir -p /run/$SVCNAME
|
mkdir -p /run/$SVCNAME
|
||||||
chown -R nobody:nogroup /run/$SVCNAME
|
chown -R nobody:nogroup /run/$SVCNAME
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# vim: set ft=sh noet ai ts=4 sw=4 sts=4:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"blacklist": [
|
"blacklist": [
|
||||||
{
|
{
|
||||||
# Пустой список означает глобальные баны, которые применяются ко всем комнатам, где присутствует бот
|
# Пустой список означает глобальные баны, которые применяются ко всем комнатам, где присутствует бот.
|
||||||
"room_name": "",
|
"room_name": "",
|
||||||
|
|
||||||
# Записывать ли в reason в банлисте комнаты сообщение вида autoban at yyyy.MM.dd hh:mm:ss
|
# Записывать ли в reason в банлисте комнаты сообщение вида autoban at yyyy.MM.dd hh:mm:ss.
|
||||||
# Если не задано, то false
|
# Если не задано, то 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)?@",
|
||||||
@@ -16,16 +16,24 @@
|
|||||||
"^(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": [
|
"phrase_re": [
|
||||||
"^Exterminate.$"
|
"^Exterminate.$"
|
||||||
]
|
],
|
||||||
|
|
||||||
|
"user_agent": [
|
||||||
|
{
|
||||||
|
"name": "BadUserAgent",
|
||||||
|
"version": "0.666.0",
|
||||||
|
"os": "DeathStarOS"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* vim: set ft=text noet ai ts=2 sw=2 sts=2: */
|
||||||
{
|
{
|
||||||
# Фронт-энд бота
|
# Фронт-энд бота
|
||||||
"jabber": {
|
"jabber": {
|
||||||
@@ -66,8 +67,46 @@
|
|||||||
|
|
||||||
# Jabber-каналы, к которым бот попробует присоединиться при старте
|
# Jabber-каналы, к которым бот попробует присоединиться при старте
|
||||||
"channels": [
|
"channels": [
|
||||||
"my_channel@conference.jabber.tld",
|
{
|
||||||
"another_channel@conference.jabber.tld"
|
# Название канала, обязательное поле
|
||||||
|
"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"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
# Статус, с которым бот запускается. (если всё нормально, этот статус будет видать только в логах сервера и
|
# Статус, с которым бот запускается. (если всё нормально, этот статус будет видать только в логах сервера и
|
||||||
@@ -151,10 +190,22 @@
|
|||||||
# command sign символ-суффикс команды, например !rehash или !help. По-умолчанию "!".
|
# command sign символ-суффикс команды, например !rehash или !help. По-умолчанию "!".
|
||||||
"csign" : "!",
|
"csign" : "!",
|
||||||
|
|
||||||
# Многословность логов. Если не задано, то info. Debug - ДЕЙСТВИТЕЛЬНО вербозный уровень логгирования.
|
# Многословность логов. Если не задано, то info.
|
||||||
# Есть ещё trace :)
|
# Есть разные уровни trace, debug, debug, info, warn, error, fatal.
|
||||||
|
# Практически задействованы только trace, debug, info.
|
||||||
"loglevel": "debug",
|
"loglevel": "debug",
|
||||||
|
|
||||||
# Если не задан, логи идут в STDOUT
|
# Если не задан, логи идут в STDOUT
|
||||||
"log": "/var/log/buny-jabber-bot/error.log"
|
"log": "/var/log/buny-jabber-bot/error.log",
|
||||||
|
|
||||||
|
# Некоторый дебаг создаёт лишний шум. Самые "шумные" опции вынесены сюда.
|
||||||
|
"debug": {
|
||||||
|
# Это дебаг самой либы go-xmpp. Это бывает полезно, если надо похэндлить какой-то формат xmpp-сообщений, т.к.
|
||||||
|
# логгирует то, что пишется "в провод"
|
||||||
|
"lib": false,
|
||||||
|
# Дебаг периодических пингов. Мало полезная вещь, разве что для выявления сетевых аномалий.
|
||||||
|
"ping": false,
|
||||||
|
# Дебаг работы presence db.
|
||||||
|
"presence_db": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
696
event_parser.go
696
event_parser.go
@@ -1,696 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/eleksir/go-xmpp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки).
|
|
||||||
func parseEvent(e interface{}) { //nolint:maintidx,gocognit,gocyclo
|
|
||||||
lastServerActivity = time.Now().Unix()
|
|
||||||
|
|
||||||
switch v := e.(type) {
|
|
||||||
// Сообщение в чяти
|
|
||||||
case xmpp.Chat:
|
|
||||||
log.Debugf("Looks like message, ChatType: %s, From: %s, Subject: %s Text: %s",
|
|
||||||
v.Type, v.Remote, v.Subject, v.Text)
|
|
||||||
|
|
||||||
// Топик чятика присылается в виде сообщения с subject, но без text
|
|
||||||
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text
|
|
||||||
if v.Text != "" {
|
|
||||||
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение
|
|
||||||
switch v.Type { //nolint:wsl,whitespace
|
|
||||||
|
|
||||||
// Групповой чятик
|
|
||||||
case "groupchat":
|
|
||||||
log.Debugf("Message from public chat: %s", v.Text)
|
|
||||||
// room := strings.SplitN(v.Remote, "/", 2)[0]
|
|
||||||
nick := strings.SplitN(v.Remote, "/", 2)[1]
|
|
||||||
|
|
||||||
if nick == config.Jabber.Nick {
|
|
||||||
log.Debug("Skipping message from myself")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd(v); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bunyChat(v); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastActivity = lastServerActivity
|
|
||||||
|
|
||||||
if muc, _ := strings.CutSuffix(v.Remote, "/"); muc != "" {
|
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Приватный чятик
|
|
||||||
case "chat":
|
|
||||||
// Здесь у нас может быть 2 вида From:
|
|
||||||
// slackware-current@conference.jabber.ru/eleksir - это если сообщение из публичного чятика
|
|
||||||
// eleksir@jabber.ru/array.lan - это если мы работаем через ростер
|
|
||||||
log.Debugf("Private message: %s", v.Text)
|
|
||||||
|
|
||||||
if err := cmd(v); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastActivity = lastServerActivity
|
|
||||||
|
|
||||||
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
|
|
||||||
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика
|
|
||||||
case "error":
|
|
||||||
// Это ошибка со стороны сервера, видимо, в ответ на наши действия и поскольку такие вещи происходят
|
|
||||||
// асинхронно, связать их с конкретными нашими действиями довольно сложно.
|
|
||||||
log.Warn(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IQ-сообщение - пинг, понг, что-то ещё...
|
|
||||||
case xmpp.IQ:
|
|
||||||
// По правилам IQ обязательно должна содержать ID, причём не пустой
|
|
||||||
if v.ID == "" {
|
|
||||||
log.Info("Got an IQ stanza with empty id, discarding")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v.Type {
|
|
||||||
case xmpp.IQTypeGet:
|
|
||||||
log.Debug("Looks like IQ get query")
|
|
||||||
|
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
|
||||||
if slices.Contains(roomsConnected, muc) {
|
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
iqStruct jabberSimpleIqGetQuery
|
|
||||||
parseError bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
|
||||||
switch iqStruct.Xmlns { //nolint:wsl,whitespace
|
|
||||||
|
|
||||||
// Запрос номера версии приложения
|
|
||||||
case "jabber:iq:version":
|
|
||||||
log.Infof("Got IQ get request for version from %s", v.From)
|
|
||||||
|
|
||||||
if id, err := talk.IqVersionResponse(
|
|
||||||
v,
|
|
||||||
"buny-jabber-bot",
|
|
||||||
"1.dev",
|
|
||||||
(strings.SplitN(runtime.GOOS, "/", 2))[0],
|
|
||||||
); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send version info to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
// Нам прислали попингуй
|
|
||||||
case "urn:xmpp:ping":
|
|
||||||
log.Infof("Got IQ get request for pong from %s", v.From)
|
|
||||||
|
|
||||||
if id, err := talk.PingResponse(v); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send pong to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
// У нас запросили время последней активности. В нашем случае это либо время запуска клиента, либо время
|
|
||||||
// последней фразы в чяти. Xep-0012.
|
|
||||||
case "jabber:iq:last":
|
|
||||||
log.Infof("Got IQ get last activity time request from %s", v.From)
|
|
||||||
|
|
||||||
if id, err := talk.JabberIqLastResponse(v, lastActivity); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send last activity time to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
// Запрос на список поддерживаемых фич - версия, локальное время, итд
|
|
||||||
case xmpp.XMPPNS_DISCO_INFO:
|
|
||||||
log.Infof("Got IQ get disco#info request from %s", v.From)
|
|
||||||
|
|
||||||
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\">"
|
|
||||||
answer += "<feature var=\"jabber:iq:version\" />"
|
|
||||||
answer += "<feature var=\"urn:xmpp:time\" />"
|
|
||||||
answer += "<feature var=\"urn:xmpp:ping\" />"
|
|
||||||
answer += "<feature var=\"jabber:iq:last\" />"
|
|
||||||
answer += "<feature var=\"http://jabber.org/protocol/disco#info\" />"
|
|
||||||
answer += "</query>"
|
|
||||||
|
|
||||||
if id, err := talk.RawInformation(
|
|
||||||
v.To,
|
|
||||||
v.From,
|
|
||||||
v.ID,
|
|
||||||
xmpp.IQTypeResult,
|
|
||||||
answer,
|
|
||||||
); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send disco#info to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
// Спрашивают список команд, чтобы поуправлять этим клиентом (xep-0030)
|
|
||||||
case xmpp.XMPPNS_DISCO_ITEMS:
|
|
||||||
log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From)
|
|
||||||
|
|
||||||
if _, err := talk.ErrorServiceUnavailable(
|
|
||||||
v,
|
|
||||||
"http://jabber.org/protocol/disco#info",
|
|
||||||
"http://jabber.org/protocol/commands",
|
|
||||||
); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send disco#items to jabber server: id=%s, err=%w",
|
|
||||||
v.ID,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
// Запрашивают что-то, о чём мы не имеем представления
|
|
||||||
default:
|
|
||||||
log.Info("Got an unknown IQ get request, discarding")
|
|
||||||
log.Info(spew.Sdump(e))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parseError = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Попробуем распарсить входящий запрос как urn:xmpp:time
|
|
||||||
if parseError {
|
|
||||||
var iqStruct jabberTimeIqGetQuery
|
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
|
||||||
switch iqStruct.Xmlns {
|
|
||||||
case "urn:xmpp:time":
|
|
||||||
log.Infof("Got IQ get time request from %s", v.From)
|
|
||||||
|
|
||||||
if id, err := talk.UrnXMPPTimeResponse(v, "+00:00"); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send urn:xmpp:time to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Info("Got bogus IQ get time request, discarding")
|
|
||||||
log.Info(spew.Sdump(e))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parseError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub
|
|
||||||
if parseError {
|
|
||||||
var iqStruct jabberPubsubIQGetQuery
|
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
|
||||||
switch iqStruct.Xmlns {
|
|
||||||
case xmpp.XMPPNS_PUBSUB:
|
|
||||||
log.Infof("Got IQ get pubsub request from %s, answer feature unimplemented", v.From)
|
|
||||||
|
|
||||||
if id, err := talk.ErrorNotImplemented(
|
|
||||||
v,
|
|
||||||
"http://jabber.org/protocol/pubsub#errors",
|
|
||||||
"subscribe",
|
|
||||||
); err != nil {
|
|
||||||
err := fmt.Errorf("unable to send pubsub feature unimplemented to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Info("Got unknown IQ get pubsub something request, discarding")
|
|
||||||
log.Info(spew.Sdump(e))
|
|
||||||
parseError = true //nolint:wsl
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parseError = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Попробуем распарсить входящий запрос как urn:xmpp:ping он приходит при MUC Self-Ping (Schrödinger's Chat)
|
|
||||||
// Предполагается, что такие респонсы должны приходить только для пинга без серверной оптимизации.
|
|
||||||
// Хотя идейно мы не поддерживаем работу без серверной оптимизации, но на пинг ответим, нам несложно.
|
|
||||||
if parseError {
|
|
||||||
var iqStruct jabberIqPing
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if id, err := talk.PingResponse(v); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send pong to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Не знаю, как парсить... залоггируем это дело и пойдём дальше
|
|
||||||
if parseError {
|
|
||||||
log.Infof("Does not look like parsable via iqStruct")
|
|
||||||
log.Info(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
case xmpp.IQTypeResult:
|
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
|
||||||
if slices.Contains(roomsConnected, muc) {
|
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это)
|
|
||||||
case v.From == config.Jabber.Server && v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
|
||||||
log.Debugf("Got S2C pong answer from %s to %s", v.From, v.To)
|
|
||||||
serverPingTimestampRx = time.Now().Unix() //nolint:wsl
|
|
||||||
|
|
||||||
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован
|
|
||||||
case v.To == talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
|
||||||
mucNameMatch := slices.Contains(roomsConnected, v.From)
|
|
||||||
|
|
||||||
if mucNameMatch {
|
|
||||||
log.Debugf("Got server-optimized MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
|
||||||
} else {
|
|
||||||
mucNickMatch := false
|
|
||||||
|
|
||||||
for _, room := range roomsConnected {
|
|
||||||
mucNick := fmt.Sprintf("%s/%s", room, config.Jabber.Nick)
|
|
||||||
|
|
||||||
if v.From == mucNick {
|
|
||||||
mucNickMatch = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mucNickMatch {
|
|
||||||
log.Debugf("Got 2-nd stage MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
|
||||||
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ
|
|
||||||
} else {
|
|
||||||
log.Infof("Got unknown pong from %s to %s", v.From, v.To)
|
|
||||||
log.Debug(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подтверждение бана
|
|
||||||
case v.To == talk.JID() && v.ID == "ban1":
|
|
||||||
mucNickMatch := false
|
|
||||||
|
|
||||||
for _, room := range roomsConnected {
|
|
||||||
mucNick := fmt.Sprintf("%s/%s", room, config.Jabber.Nick)
|
|
||||||
|
|
||||||
if v.From == mucNick {
|
|
||||||
mucNickMatch = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Формально, ответ должен парситься как пустой result и совпадать с типом jabberSimpleIqGetQuery
|
|
||||||
// Ответ приходит с 2 xmlns, похоже, он парсится неправильно в go-xmpp.
|
|
||||||
// TODO: после исправления go-xmpp, надо исправить это тут
|
|
||||||
var iqStruct jabberSimpleIqGetQuery
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Info("Got an IQ result. Dunno how deal with it, discarding")
|
|
||||||
log.Debug(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем
|
|
||||||
case xmpp.IQTypeSet:
|
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
|
||||||
if slices.Contains(roomsConnected, muc) {
|
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Got an IQ request for set something. Answer not implemented")
|
|
||||||
|
|
||||||
if id, err := talk.ErrorNotImplemented(
|
|
||||||
v,
|
|
||||||
"http://jabber.org/protocol/commands",
|
|
||||||
xmpp.IQTypeSet,
|
|
||||||
); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"unable to send set feature unimplemented to jabber server: id=%s, err=%w",
|
|
||||||
id,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug(spew.Sdump(e))
|
|
||||||
|
|
||||||
// Нам прилетело сообщение об ошибке
|
|
||||||
case xmpp.IQTypeError:
|
|
||||||
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
|
||||||
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
|
||||||
var (
|
|
||||||
iqPingStruct jabberIqPing
|
|
||||||
parseError = true
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
|
||||||
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
|
||||||
if v.From == config.Jabber.Server {
|
|
||||||
msg := "Server announced that it can answer c2s ping, but gives us an error to such query, "
|
|
||||||
msg += "fallback to keepalive whitespace pings"
|
|
||||||
log.Error(msg)
|
|
||||||
|
|
||||||
serverCapsList.Set("urn:xmpp:ping", false)
|
|
||||||
} else {
|
|
||||||
log.Errorf("Got 'ping unsupported' message from: %s to: %s", v.From, v.To)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Error("Iq parsed as ping, but does not belong to xmlns urn:xmpp:ping")
|
|
||||||
log.Error(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться
|
|
||||||
if parseError {
|
|
||||||
var iqErrorCancelNotAcceptable jabberIqErrorCancelNotAcceptable
|
|
||||||
|
|
||||||
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
|
||||||
if v.To == talk.JID() {
|
|
||||||
nick := strings.SplitN(v.From, "/", 2)[1]
|
|
||||||
|
|
||||||
if slices.Contains(roomsConnected, iqErrorCancelNotAcceptable.By) && nick == config.Jabber.Nick {
|
|
||||||
log.Errorf(
|
|
||||||
"Got Iq error message from: %s to: %s. Looks like i'm not in MUC anymore",
|
|
||||||
v.From, v.To,
|
|
||||||
)
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(config.Jabber.MucRejoinDelay) * time.Second)
|
|
||||||
|
|
||||||
if _, err := talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil {
|
|
||||||
err := fmt.Errorf(
|
|
||||||
"looks like connection to server also lost err=%w",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Error(
|
|
||||||
"looks like message parsed as jabberIqErrorCancelNotAcceptable but we're not in given room",
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Error(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Error(
|
|
||||||
"Looks like message parsed as jabberIqErrorCancelNotAcceptable but addressed not to us",
|
|
||||||
)
|
|
||||||
|
|
||||||
log.Error(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
parseError = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if parseError {
|
|
||||||
log.Error("Unhandled IQ Error message")
|
|
||||||
log.Error(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
// Нам прилетело что-то неизвестное из семейства IQ stanza
|
|
||||||
default:
|
|
||||||
log.Info("Got an unknown IQ request. Dunno how deal with it, discarding")
|
|
||||||
log.Info(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Смена статуса участника
|
|
||||||
case xmpp.Presence:
|
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
|
||||||
if slices.Contains(roomsConnected, muc) {
|
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if v.Type == xmpp.IQTypeError {
|
|
||||||
// Это событие происходит, когда из чятика выходит другой инстанс клиента.
|
|
||||||
// Такое бывает, когда 2 инстанса с одинаковым ресурсом, например, начинают "драться" за возможность
|
|
||||||
// остаться на сервере. Ситуация, которую допускать нельзя, на самом деле, потому что рано или поздно такого
|
|
||||||
// клиента забанят из-за спама "вошёл-вышел".
|
|
||||||
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
|
|
||||||
if v.Type == "unavailable" {
|
|
||||||
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть
|
|
||||||
roomPresences.Delete(v.From)
|
|
||||||
|
|
||||||
log.Error("Presence notification - looks like another instance of client leaves room")
|
|
||||||
|
|
||||||
if slices.Contains(roomsConnected, v.From) {
|
|
||||||
go joinMuc(v.From)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
|
|
||||||
v.Type, v.From, v.Show, v.Status)
|
|
||||||
log.Errorf(spew.Sdump(v))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Infof(
|
|
||||||
"Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s",
|
|
||||||
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID,
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
if v.Show == "" && v.Status == "" {
|
|
||||||
if nick == config.Jabber.Nick {
|
|
||||||
roomsConnected = append(roomsConnected, room)
|
|
||||||
// На всякий случай дедуплицируем список комнат, к которым мы заджойнились.
|
|
||||||
sort.Strings(roomsConnected)
|
|
||||||
slices.Compact(roomsConnected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v.Role {
|
|
||||||
// Участник ушёл
|
|
||||||
case "none":
|
|
||||||
if presenceJSONInterface, present := roomPresences.Get(room); present {
|
|
||||||
presenceJSONStrings := interfaceToStringSlice(presenceJSONInterface)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
roomPresences.Set(room, newPresenceJSONStrings)
|
|
||||||
}
|
|
||||||
// Участник пришёл
|
|
||||||
default:
|
|
||||||
var presenceJSONStrings []string
|
|
||||||
var newPresenceJSONStrings []string
|
|
||||||
var presenceJSONBytes []byte
|
|
||||||
|
|
||||||
if presenceJSONInterface, present := roomPresences.Get(room); present {
|
|
||||||
presenceJSONStrings = interfaceToStringSlice(presenceJSONInterface)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, presenceJSONString := range presenceJSONStrings {
|
|
||||||
var p xmpp.Presence
|
|
||||||
_ = json.Unmarshal([]byte(presenceJSONString), &p)
|
|
||||||
|
|
||||||
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для grouchat)
|
|
||||||
// просто замещаем его.
|
|
||||||
if p.From == v.From {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
newPresenceJSONStrings = append(newPresenceJSONStrings, presenceJSONString)
|
|
||||||
}
|
|
||||||
|
|
||||||
presenceJSONBytes, _ = json.Marshal(v) //nolint:errchkjson
|
|
||||||
newPresenceJSONStrings = append(newPresenceJSONStrings, string(presenceJSONBytes))
|
|
||||||
roomPresences.Set(room, newPresenceJSONStrings)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате.
|
|
||||||
// По правилам, мы можем что-то делать, только после того, как нам прилетит наш собственный presence, это
|
|
||||||
// значит, что мы вошли в комнату.
|
|
||||||
if slices.Contains(roomsConnected, room) {
|
|
||||||
if v.Affiliation != "outcast" {
|
|
||||||
if err := bunyPresense(v); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ответ на запрос поддерживаемых фич, который "http://jabber.org/protocol/disco#info"
|
|
||||||
case xmpp.DiscoResult:
|
|
||||||
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
|
||||||
if slices.Contains(roomsConnected, muc) {
|
|
||||||
lastMucActivity.Set(muc, lastServerActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Я видел 2 типа disco result и они отличались только []identities. Попробуем вытащить известный identity
|
|
||||||
// TODO: проверять адресата v.To
|
|
||||||
for _, ident := range v.Identities {
|
|
||||||
switch ident.Category {
|
|
||||||
case "server":
|
|
||||||
// Конкретно сейчас нас интересует только поддержка c2s ping
|
|
||||||
for _, feature := range v.Features {
|
|
||||||
log.Debugf("Server %s announced that it supports feature: %s", v.From, feature)
|
|
||||||
serverCapsList.Set(feature, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverCapsQueried = true
|
|
||||||
|
|
||||||
case "conference":
|
|
||||||
mucCaps := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, feature := range v.Features {
|
|
||||||
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
|
|
||||||
mucCaps[feature] = true //nolint:wsl
|
|
||||||
}
|
|
||||||
|
|
||||||
mucCapsList.Set(v.From, mucCaps)
|
|
||||||
|
|
||||||
case "pubsub":
|
|
||||||
log.Debugf("PubSub component %s reply to disco#info, skipping", v.From)
|
|
||||||
|
|
||||||
default:
|
|
||||||
log.Debug("Got unknown reply to disco#info")
|
|
||||||
log.Debug(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Это что-то неизвестное, подампим событие в лог
|
|
||||||
default:
|
|
||||||
log.Info(spew.Sdump(e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
72
globals.go
72
globals.go
@@ -1,72 +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
|
|
||||||
|
|
||||||
// sync.Map-ка со списком участников конференций (в json-формате, согласно структуре xmpp.Presence, "room".[]json).
|
|
||||||
var roomPresences *Collection
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
16
go.mod
16
go.mod
@@ -1,18 +1,14 @@
|
|||||||
module buny-jabber-bot
|
module buny-jabber-bot
|
||||||
|
|
||||||
go 1.21
|
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.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hjson/hjson-go v3.3.0+incompatible
|
github.com/hjson/hjson-go v3.3.0+incompatible
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
github.com/xmppo/go-xmpp v0.3.1
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/crypto v0.13.0 // indirect
|
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
|
||||||
golang.org/x/text v0.13.0 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
38
go.sum
38
go.sum
@@ -1,28 +1,18 @@
|
|||||||
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.6.0 h1:bkGr0YVczJWpkyk/IHhZb2ewv+NeHLvdNfds0TFgqao=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/eleksir/go-xmpp v1.6.0/go.mod h1:Ii6Wemk+yuGQOF+Glt0syjvtcPvdaBLstuNp/+rRJrg=
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a h1:zSc8tkEGo49/E6cQ9o00si0L9Q/Lu1Hsg8JWKCvzEDU=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/jbrukh/bayesian v0.0.0-20231117143245-13ae6f916c7a/go.mod h1:SELxwZQq/mPnfPCR2mchLmT4TQaPJvYtLcCtDWSM7vM=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/xmppo/go-xmpp v0.2.19-0.20251123101827-6986f11d85b8 h1:Q6MDZxJVNPlwH/Pm3uQhXhdA2eSUP8YMmhSkCUHbfLU=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/xmppo/go-xmpp v0.2.19-0.20251123101827-6986f11d85b8/go.mod h1:RyX2+ufcANlJ/ItVhrvfzy4vrvr4537qpPXRsOB+Nz4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/xmppo/go-xmpp v0.3.1 h1:GZUCGZozZdfvv25FVvlgwKoNdRjmfJCUcVG8349/E4A=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/xmppo/go-xmpp v0.3.1/go.mod h1:EBLbzPt4Y9OJBEF58Lhc4IrTnO226aIkbfosQo6KWeA=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
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.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
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=
|
|
||||||
|
|||||||
108
internal/jabber/bf.go
Normal file
108
internal/jabber/bf.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Package jabber contains all buny jabber bot guts.
|
||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jbrukh/bayesian"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// goodDictionaryFile путь до файла с заведомо хорошими фразами.
|
||||||
|
goodDictionaryFile string = "./data/good_dictionary.txt" //nolint: unused
|
||||||
|
// badDictionaryFile путь до файла с заведемо плохими фразами.
|
||||||
|
badDictionaryFile string = "./data/bad_dictionary.txt" //nolint: unused
|
||||||
|
// dataFile путь до файла с данными тренировки фильтра.
|
||||||
|
dataFile string = "./data/data.bin" //nolint: unused
|
||||||
|
// Bad bayessian class with bad items.
|
||||||
|
Bad bayesian.Class = "Bad"
|
||||||
|
// Good bayessian class with good items.
|
||||||
|
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: */
|
||||||
220
internal/jabber/black-whilte_lists.go
Normal file
220
internal/jabber/black-whilte_lists.go
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/hjson/hjson-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 := filepath.Dir(executablePath) + "/data/whitelist.json"
|
||||||
|
|
||||||
|
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]any
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
blacklistJSONPath := filepath.Dir(executablePath) + "/data/blacklist.json"
|
||||||
|
|
||||||
|
locations := []string{
|
||||||
|
"~/.buny-jabber-bot-blacklist.json",
|
||||||
|
"~/buny-jabber-bot-blacklist.json",
|
||||||
|
"/etc/buny-jabber-bot-blacklist.json",
|
||||||
|
blacklistJSONPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
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]any
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range sampleBlacklist.Blacklist {
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(entry), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вообще говоря, нас уверяют, что простого "=" хватает, но на практике оказывается, что вложенные стуктуры
|
||||||
|
// могут и не скопироваться.
|
||||||
|
tmpjson, _ := json.Marshal(sampleBlacklist) //nolint: errchkjson // Да блять, сколько можно
|
||||||
|
// доёбываться, эти данные 100500 раз перекодировались, там ошибка возможна только если есть ошибка в
|
||||||
|
// encoding/json, но в этом случае мы в любом случае ничего поделать не можем, вот всяком случае в рантайме.
|
||||||
|
_ = json.Unmarshal(tmpjson, &j.BlackList)
|
||||||
|
|
||||||
|
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: */
|
||||||
653
internal/jabber/buny.go
Normal file
653
internal/jabber/buny.go
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 == "" {
|
||||||
|
msg := "We got empty room field in presence event, which kinda strange: " + spew.Sdump(v)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(msg, "\n") {
|
||||||
|
log.Info(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
goodJid := strings.SplitN(v.JID, "/", 2)[0]
|
||||||
|
|
||||||
|
// Обрабатываем правила белого списка.
|
||||||
|
for _, good := range j.WhiteList.Whitelist {
|
||||||
|
// Глобальный белый список.
|
||||||
|
if good.RoomName == "" {
|
||||||
|
if slices.Contains(good.Jid, goodJid) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Белый список для конкретных каналов.
|
||||||
|
if good.RoomName == room {
|
||||||
|
if slices.Contains(good.Jid, 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]
|
||||||
|
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() {
|
||||||
|
log.Debugf("Skipping useragent blacklist check for myself")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSON, ok := presenceJSONInterface.([]byte)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var presences []xmpp.Presence
|
||||||
|
|
||||||
|
if err := json.Unmarshal(presenceJSON, &presences); err != nil {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return nil //nolint: nilerr // Это не критическая ошибка, её не надо возвращать.
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука.
|
||||||
|
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat)
|
||||||
|
// пропускаем его.
|
||||||
|
if p.From != v.From {
|
||||||
|
log.Debugf("Skip check for %s because it is not %s", p.From, v.From)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Found jid of %s in presence db: %s", v.From, p.JID)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("Useragent is not owner or moderator, proceeding with check...")
|
||||||
|
|
||||||
|
// Lookup in global blacklist.
|
||||||
|
log.Debug("Searching our room in connected room list...")
|
||||||
|
|
||||||
|
for _, cRoom := range j.RoomsConnected {
|
||||||
|
if cRoom == room {
|
||||||
|
log.Debug("Found our room in list of connected rooms")
|
||||||
|
|
||||||
|
for _, bEntry := range j.BlackList.Blacklist {
|
||||||
|
// Обработаем правила глобального чёрного списка.
|
||||||
|
if bEntry.RoomName == "" {
|
||||||
|
log.Debugf("Check in global blacklist")
|
||||||
|
log.Debugf("We have useragent struct with Name=%s, Version=%s, OS=%s", ver.Name, ver.Version, ver.Os)
|
||||||
|
|
||||||
|
for _, useragent := range bEntry.UserAgent {
|
||||||
|
log.Debugf("We have useragent blacklist entry Name=%s, Version=%s, OS=%s", useragent.Name, useragent.Version, useragent.Os)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case useragent.Name != "" && useragent.Version != "" && useragent.Os != "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Name, Version and OS match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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 != "" && useragent.Os == "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Name and Version match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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 == "" && useragent.Os == "":
|
||||||
|
if ver.Name == useragent.Name {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Name match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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 != "" && useragent.Os == "":
|
||||||
|
if ver.Version == useragent.Version {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Version match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("No matches in global software versions blacklist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if bEntry.RoomName == room {
|
||||||
|
log.Debug("Found blacklist entries for room")
|
||||||
|
|
||||||
|
for _, useragent := range bEntry.UserAgent {
|
||||||
|
log.Debugf("We have useragent blacklist entry Name=%s, Version=%s, OS=%s", useragent.Name, useragent.Version, useragent.Os)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case useragent.Name != "" && useragent.Version != "" && useragent.Os != "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version && ver.Os == useragent.Os {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Name, Version and OS match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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 != "" && useragent.Os == "":
|
||||||
|
if ver.Name == useragent.Name && ver.Version == useragent.Version {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Name and Version match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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 == "" && useragent.Os == "":
|
||||||
|
if ver.Name == useragent.Name {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Name match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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 != "" && useragent.Os == "":
|
||||||
|
if ver.Version == useragent.Version {
|
||||||
|
log.Warnf("Hammer falls on %s (%s): Software Version match entry in blacklist", p.From, p.JID)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("No matches in software versions blacklist for %s", room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
package main
|
package jabber
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
|
type (
|
||||||
type Collection struct {
|
// Collection это sync.Map-ка, хранящая в себе произвольные итемы.
|
||||||
items sync.Map
|
Collection struct {
|
||||||
close chan struct{}
|
close chan struct{}
|
||||||
}
|
items sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
// Item представляет собой интерфейс для хранения произвольных данных.
|
// Item представляет собой интерфейс для хранения произвольных данных.
|
||||||
type item struct {
|
item struct {
|
||||||
data interface{}
|
data any
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// NewCollection создаёт инстанс коллекции.
|
// NewCollection создаёт инстанс коллекции.
|
||||||
func NewCollection() *Collection {
|
func NewCollection() *Collection {
|
||||||
@@ -25,7 +27,7 @@ func NewCollection() *Collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get достаёт данные по заданному ключу из коллекции.
|
// Get достаёт данные по заданному ключу из коллекции.
|
||||||
func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
func (collection *Collection) Get(key any) (any, bool) {
|
||||||
obj, exists := collection.items.Load(key)
|
obj, exists := collection.items.Load(key)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -38,15 +40,15 @@ func (collection *Collection) Get(key interface{}) (interface{}, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set сохраняет данные с заданным ключом в коллекцию.
|
// Set сохраняет данные с заданным ключом в коллекцию.
|
||||||
func (collection *Collection) Set(key interface{}, value interface{}) {
|
func (collection *Collection) Set(key any, value any) {
|
||||||
collection.items.Store(key, item{
|
collection.items.Store(key, item{
|
||||||
data: value,
|
data: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Range применяет функцию f ко всем ключам в коллекции.
|
// Range применяет функцию f ко всем ключам в коллекции.
|
||||||
func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
func (collection *Collection) Range(f func(key, value any) bool) {
|
||||||
fn := func(key, value interface{}) bool {
|
fn := func(key, value any) bool {
|
||||||
item := value.(item)
|
item := value.(item)
|
||||||
|
|
||||||
return f(key, item.data)
|
return f(key, item.data)
|
||||||
@@ -56,13 +58,14 @@ func (collection *Collection) Range(f func(key, value interface{}) bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete удаляет ключ и значение из коллекции данных.
|
// Delete удаляет ключ и значение из коллекции данных.
|
||||||
func (collection *Collection) Delete(key interface{}) {
|
func (collection *Collection) Delete(key any) {
|
||||||
collection.items.Delete(key)
|
collection.items.Delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close очищает и высвобождает ресурсы, занятые коллекцией.
|
// Close очищает и высвобождает ресурсы, занятые коллекцией.
|
||||||
func (collection *Collection) Close() {
|
func (collection *Collection) Close() {
|
||||||
collection.close <- struct{}{}
|
collection.close <- struct{}{}
|
||||||
|
|
||||||
collection.items = sync.Map{}
|
collection.items = sync.Map{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,37 +1,40 @@
|
|||||||
package main
|
package jabber
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/eleksir/go-xmpp"
|
"github.com/xmppo/go-xmpp"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func cmd(v xmpp.Chat) error {
|
// Cmd парсит команды из чятика.
|
||||||
|
func (j *Jabber) Cmd(v xmpp.Chat) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch {
|
switch v.Text {
|
||||||
case v.Text == fmt.Sprintf("%shelp", config.CSign) || v.Text == fmt.Sprintf("%sпомощь", config.CSign):
|
case j.C.CSign + "help", j.C.CSign + "помощь":
|
||||||
var (
|
var (
|
||||||
answer string
|
answer string
|
||||||
chosenOneTalks = false
|
chosenOneTalks = false
|
||||||
)
|
)
|
||||||
|
|
||||||
realJID := getRealJIDfromNick(v.Remote)
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
for _, master := range config.Jabber.BotMasters {
|
for _, master := range j.C.Jabber.BotMasters {
|
||||||
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
||||||
chosenOneTalks = true
|
chosenOneTalks = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if chosenOneTalks {
|
if chosenOneTalks {
|
||||||
answer = fmt.Sprintf("%sпомощь - этот список команд\n", config.CSign)
|
answer = j.C.CSign + "помощь - этот список команд\n"
|
||||||
answer += fmt.Sprintf("%shelp - this commands list\n", config.CSign)
|
|
||||||
answer += fmt.Sprintf("%srehash - reload white and black lists", config.CSign)
|
answer += j.C.CSign + "help - this commands list\n"
|
||||||
|
answer += j.C.CSign + "rehash - reload white and black lists (available to bot admins only)\n"
|
||||||
|
answer += j.C.CSign + "ver | " + j.C.CSign + "version - prints version of software"
|
||||||
} else {
|
} else {
|
||||||
answer = "Ничем помочь не могу. Луна не светит на тебя."
|
answer = "Ничем помочь не могу. Луна не светит на тебя."
|
||||||
}
|
}
|
||||||
@@ -42,7 +45,7 @@ func cmd(v xmpp.Chat) error {
|
|||||||
dest = (strings.SplitN(v.Remote, "/", 2))[0]
|
dest = (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := talk.Send(
|
if _, err := j.Talk.Send(
|
||||||
xmpp.Chat{ //nolint:exhaustruct
|
xmpp.Chat{ //nolint:exhaustruct
|
||||||
Remote: dest,
|
Remote: dest,
|
||||||
Text: strings.TrimSpace(answer),
|
Text: strings.TrimSpace(answer),
|
||||||
@@ -54,15 +57,14 @@ func cmd(v xmpp.Chat) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case v.Text == fmt.Sprintf("%srehash", config.CSign):
|
case j.C.CSign + "rehash":
|
||||||
/* Эта команда может прилетать из приватной беседы с realjid-ом, из приватной беседы с chat-nick-ом, а также из
|
/* Эта команда может прилетать из приватной беседы с realjid-ом, из приватной беседы с chat-nick-ом, а также из
|
||||||
* чятика.
|
* чятика.
|
||||||
* Groupchat мы можем определить по v.Type (groupchat) и тогда понятно что делать.
|
* Groupchat мы можем определить по v.Type (groupchat) и тогда понятно что делать.
|
||||||
* А вот, например, приватные притязания определить несколько сложно-вато. Но можно попробовать, например,
|
* А вот, например, приватные притязания определить несколько сложно-вато. Но можно попробовать, например,
|
||||||
* пытаться по списку presence-ов находить персонажа и его real jid и уже из этого определять master он или нет.
|
* пытаться по списку presence-ов находить персонажа и его real jid и уже из этого определять master он или нет.
|
||||||
*/
|
*/
|
||||||
|
// Попробуем определить каков алгоритм наших действий, в зависимости от типа сообщения.
|
||||||
// Попробуем определить каков алгоритм наших действий, в зависимости от типа сообщения
|
|
||||||
switch v.Type {
|
switch v.Type {
|
||||||
case "groupchat":
|
case "groupchat":
|
||||||
// Проверим, является ли "командир" избранным или это самозванец?
|
// Проверим, является ли "командир" избранным или это самозванец?
|
||||||
@@ -71,9 +73,9 @@ func cmd(v xmpp.Chat) error {
|
|||||||
listsLoaded = true
|
listsLoaded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
realJID := getRealJIDfromNick(v.Remote)
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
for _, master := range config.Jabber.BotMasters {
|
for _, master := range j.C.Jabber.BotMasters {
|
||||||
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
||||||
chosenOneTalks = true
|
chosenOneTalks = true
|
||||||
}
|
}
|
||||||
@@ -82,36 +84,46 @@ func cmd(v xmpp.Chat) error {
|
|||||||
if !chosenOneTalks {
|
if !chosenOneTalks {
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"Command %srehash given by non-bot_master user %s(%s), ignoring",
|
"Command %srehash given by non-bot_master user %s(%s), ignoring",
|
||||||
config.CSign,
|
j.C.CSign,
|
||||||
realJID,
|
realJID,
|
||||||
v.Remote,
|
v.Remote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = "Ничем помочь не могу. Луна не светит на тебя."
|
||||||
|
|
||||||
|
_, err := j.Talk.Send(msg)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := readWhitelist(); err != nil {
|
if err := j.ReadWhitelist(); err != nil {
|
||||||
listsLoaded = false
|
listsLoaded = false
|
||||||
|
|
||||||
var msg xmpp.Chat
|
var msg xmpp.Chat
|
||||||
|
|
||||||
msg.Remote = v.Remote
|
msg.Remote = v.Remote
|
||||||
msg.Type = "chat" // private message
|
msg.Type = "chat" // private message.
|
||||||
msg.Text = fmt.Sprint(err)
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
if _, err := talk.Send(msg); err != nil {
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := readBlacklist(); err != nil {
|
if err := j.ReadBlacklist(); err != nil {
|
||||||
listsLoaded = false
|
listsLoaded = false
|
||||||
|
|
||||||
var msg xmpp.Chat
|
var msg xmpp.Chat
|
||||||
|
|
||||||
msg.Remote = v.Remote
|
msg.Remote = v.Remote
|
||||||
msg.Type = "chat" // private message
|
msg.Type = "chat" // private message.
|
||||||
msg.Text = fmt.Sprint(err)
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
if _, err := talk.Send(msg); err != nil {
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,28 +132,30 @@ func cmd(v xmpp.Chat) error {
|
|||||||
room := (strings.SplitN(v.Remote, "/", 2))[0]
|
room := (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
|
||||||
var msg xmpp.Chat
|
var msg xmpp.Chat
|
||||||
|
|
||||||
msg.Remote = room
|
msg.Remote = room
|
||||||
msg.Type = v.Type
|
msg.Type = v.Type
|
||||||
msg.Text = "Сделано"
|
msg.Text = "Сделано"
|
||||||
|
|
||||||
if _, err := talk.Send(msg); err != nil {
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "chat":
|
case "chat":
|
||||||
room := (strings.SplitN(v.Remote, "/", 2))[0]
|
room := (strings.SplitN(v.Remote, "/", 2))[0]
|
||||||
|
|
||||||
// Реагировать на приватную команду "из комнаты", только если бот в комнате
|
// Реагировать на приватную команду "из комнаты", только если бот в комнате.
|
||||||
if slices.Contains(roomsConnected, room) {
|
if slices.Contains(j.RoomsConnected, room) {
|
||||||
var (
|
var (
|
||||||
chosenOneTalks = false
|
chosenOneTalks = false
|
||||||
listsLoaded = true
|
listsLoaded = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// Реагировать на rehash только если собеседник является bot master-ом
|
// Реагировать на rehash только если собеседник является bot master-ом.
|
||||||
realJID := getRealJIDfromNick(v.Remote)
|
realJID := j.GetRealJIDfromNick(v.Remote)
|
||||||
|
|
||||||
for _, master := range config.Jabber.BotMasters {
|
for _, master := range j.C.Jabber.BotMasters {
|
||||||
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
if (strings.SplitN(realJID, "/", 2))[0] == master {
|
||||||
chosenOneTalks = true
|
chosenOneTalks = true
|
||||||
}
|
}
|
||||||
@@ -150,60 +164,102 @@ func cmd(v xmpp.Chat) error {
|
|||||||
if !chosenOneTalks {
|
if !chosenOneTalks {
|
||||||
log.Infof(
|
log.Infof(
|
||||||
"Command %srehash given by non-bot_master user %s(%s), ignoring",
|
"Command %srehash given by non-bot_master user %s(%s), ignoring",
|
||||||
config.CSign,
|
j.C.CSign,
|
||||||
realJID,
|
realJID,
|
||||||
v.Remote,
|
v.Remote,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var msg xmpp.Chat
|
||||||
|
|
||||||
|
msg.Remote = v.Remote
|
||||||
|
msg.Type = v.Type
|
||||||
|
msg.Text = "Ничем помочь не могу. Луна не светит на тебя."
|
||||||
|
|
||||||
|
_, err := j.Talk.Send(msg)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := readWhitelist(); err != nil {
|
if err := j.ReadWhitelist(); err != nil {
|
||||||
listsLoaded = false
|
listsLoaded = false
|
||||||
|
|
||||||
var msg xmpp.Chat
|
var msg xmpp.Chat
|
||||||
|
|
||||||
msg.Remote = v.Remote
|
msg.Remote = v.Remote
|
||||||
msg.Type = v.Type
|
msg.Type = v.Type
|
||||||
msg.Text = fmt.Sprint(err)
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
if _, err := talk.Send(msg); err != nil {
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := readBlacklist(); err != nil {
|
if err := j.ReadBlacklist(); err != nil {
|
||||||
listsLoaded = false
|
listsLoaded = false
|
||||||
|
|
||||||
var msg xmpp.Chat
|
var msg xmpp.Chat
|
||||||
|
|
||||||
msg.Remote = v.Remote
|
msg.Remote = v.Remote
|
||||||
msg.Type = v.Type
|
msg.Type = v.Type
|
||||||
msg.Text = fmt.Sprint(err)
|
msg.Text = fmt.Sprint(err)
|
||||||
|
|
||||||
if _, err := talk.Send(msg); err != nil {
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if listsLoaded {
|
if listsLoaded {
|
||||||
var msg xmpp.Chat
|
var msg xmpp.Chat
|
||||||
|
|
||||||
msg.Remote = v.Remote
|
msg.Remote = v.Remote
|
||||||
msg.Type = v.Type
|
msg.Type = v.Type
|
||||||
msg.Text = "Сделано"
|
msg.Text = "Сделано"
|
||||||
|
|
||||||
if _, err := talk.Send(msg); err != nil {
|
if _, err := j.Talk.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.Infof("Rehash command from outer space: %s", spew.Sdump(v))
|
msg := "Rehash command from outer space: " + spew.Sdump(v)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(msg, "\n") {
|
||||||
|
log.Info(str)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
case j.C.CSign + "ver", j.C.CSign + "version":
|
||||||
|
var (
|
||||||
|
answer = "Version " + 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:
|
default:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
889
internal/jabber/event_parser.go
Normal file
889
internal/jabber/event_parser.go
Normal file
@@ -0,0 +1,889 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseEvent парсит ивенты, прилетающие из модуля xmpp (изменения presence, фразы участников, ошибки).
|
||||||
|
func (j *Jabber) ParseEvent(e any) { //nolint:maintidx,gocognit,gocyclo
|
||||||
|
j.LastServerActivity = time.Now().Unix()
|
||||||
|
|
||||||
|
switch v := e.(type) {
|
||||||
|
// Сообщение в чяти.
|
||||||
|
case xmpp.Chat:
|
||||||
|
log.Debugf("Looks like message, ChatType: %s, From: %s, Subject: %s Text: %s",
|
||||||
|
v.Type, v.Remote, v.Subject, v.Text)
|
||||||
|
|
||||||
|
// Топик чятика присылается в виде сообщения с subject, но без text.
|
||||||
|
// В то же время сообщения от людей приходят с пустым subject, но с заполненным text.
|
||||||
|
if v.Text != "" {
|
||||||
|
// Чятики бывают групповые и не групповые, от этого зависит Remote, куда направлять сообщение.
|
||||||
|
switch v.Type { //nolint:wsl,whitespace
|
||||||
|
// Групповой чятик.
|
||||||
|
case "groupchat":
|
||||||
|
var (
|
||||||
|
room = strings.SplitN(v.Remote, "/", 2)[0]
|
||||||
|
nick = strings.SplitN(v.Remote, "/", 2)[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Debugf("Message from public chat: %s", v.Text)
|
||||||
|
|
||||||
|
if nick == j.GetBotNickFromRoomConfig(room) {
|
||||||
|
log.Debug("Skipping message from myself")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "" {
|
||||||
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Приватный чятик.
|
||||||
|
case "chat":
|
||||||
|
// Здесь у нас может быть 2 вида From:
|
||||||
|
// slackware-current@conference.jabber.ru/eleksir - это если сообщение из публичного чятика
|
||||||
|
// eleksir@jabber.ru/array.lan - это если мы работаем через ростер
|
||||||
|
log.Debugf("Private message: %s", v.Text)
|
||||||
|
|
||||||
|
if err := j.Cmd(v); err != nil {
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
j.LastActivity = j.LastServerActivity
|
||||||
|
|
||||||
|
// Внезапно, ошибки. По идее они должны ассоциироваться с отправляемыми сообщениями, но по ходу это
|
||||||
|
// не реализовано, поэтому мы получаем ошибки в форме отдельного чятика.
|
||||||
|
case "error":
|
||||||
|
// Это ошибка со стороны сервера, видимо, в ответ на наши действия и поскольку такие вещи происходят
|
||||||
|
// асинхронно, связать их с конкретными нашими действиями довольно сложно.
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Error(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IQ-сообщение - пинг, понг, что-то ещё...
|
||||||
|
case xmpp.IQ:
|
||||||
|
// По правилам IQ обязательно должна содержать ID, причём не пустой.
|
||||||
|
if v.ID == "" {
|
||||||
|
log.Info("Got an IQ stanza with empty id, discarding")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Type {
|
||||||
|
case xmpp.IQTypeGet:
|
||||||
|
log.Debug("Looks like IQ get query")
|
||||||
|
|
||||||
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
iqStruct SimpleIqGetQuery
|
||||||
|
parseError bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
|
switch iqStruct.Xmlns { //nolint:wsl,whitespace
|
||||||
|
// Запрос на список поддерживаемых фич - версия, локальное время, итд.
|
||||||
|
case xmpp.XMPPNS_DISCO_INFO:
|
||||||
|
log.Infof("Got IQ get disco#info request from %s", v.From)
|
||||||
|
|
||||||
|
answer := "<query xmlns=\"http://jabber.org/protocol/disco#info\">"
|
||||||
|
|
||||||
|
answer += "<feature var=\"jabber:iq:version\" />"
|
||||||
|
answer += "<feature var=\"urn:xmpp:time\" />"
|
||||||
|
answer += "<feature var=\"urn:xmpp:ping\" />"
|
||||||
|
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/muc\" />"
|
||||||
|
answer += "</query>"
|
||||||
|
|
||||||
|
if id, err := j.Talk.RawInformation(
|
||||||
|
v.To,
|
||||||
|
v.From,
|
||||||
|
v.ID,
|
||||||
|
xmpp.IQTypeResult,
|
||||||
|
answer,
|
||||||
|
); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to send disco#info to jabber server: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
|
||||||
|
// Спрашивают список команд, чтобы поуправлять этим клиентом (xep-0030).
|
||||||
|
case xmpp.XMPPNS_DISCO_ITEMS:
|
||||||
|
log.Infof("Got IQ get disco#items request from %s, answer service unavailable", v.From)
|
||||||
|
|
||||||
|
if _, err := j.Talk.ErrorServiceUnavailable(
|
||||||
|
v,
|
||||||
|
"http://jabber.org/protocol/disco#info",
|
||||||
|
"http://jabber.org/protocol/commands",
|
||||||
|
); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to send disco#items to jabber server: id=%s, err=%w",
|
||||||
|
v.ID,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
|
||||||
|
// Запрашивают что-то, о чём мы не имеем представления.
|
||||||
|
default:
|
||||||
|
log.Info("Got an unknown IQ get request, discarding")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parseError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попробуем распарсить входящий запрос как urn:xmpp:time.
|
||||||
|
if parseError {
|
||||||
|
var iqStruct TimeIqGetQuery
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
|
switch iqStruct.Xmlns {
|
||||||
|
case "urn:xmpp:time":
|
||||||
|
log.Infof("Got IQ get time request from %s", v.From)
|
||||||
|
|
||||||
|
if id, err := j.Talk.UrnXMPPTimeResponse(v, "+00:00"); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to send urn:xmpp:time to jabber server: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Info("Got bogus IQ get time request, discarding")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parseError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попробуем распарсить входящий запрос как http://jabber.org/protocol/pubsub.
|
||||||
|
if parseError {
|
||||||
|
var iqStruct PubsubIQGetQuery
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &iqStruct); err == nil {
|
||||||
|
switch iqStruct.Xmlns {
|
||||||
|
case xmpp.XMPPNS_PUBSUB:
|
||||||
|
log.Infof("Got IQ get pubsub request from %s, answer feature unimplemented", v.From)
|
||||||
|
|
||||||
|
if id, err := j.Talk.ErrorNotImplemented(
|
||||||
|
v,
|
||||||
|
"http://jabber.org/protocol/pubsub#errors",
|
||||||
|
"subscribe",
|
||||||
|
); err != nil {
|
||||||
|
err := fmt.Errorf("unable to send pubsub feature unimplemented to jabber server: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Info("Got unknown IQ get pubsub something request, discarding")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = true //nolint:wsl
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parseError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не знаю, как парсить... залоггируем это дело и пойдём дальше.
|
||||||
|
if parseError {
|
||||||
|
log.Infof("Does not look like parsable via iqStruct")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case xmpp.IQTypeResult:
|
||||||
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// Похоже на pong от сервера (по стандарту в ответе нету query, но go-xmpp нам подсовывает это).
|
||||||
|
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)
|
||||||
|
|
||||||
|
j.ServerPingTimestampRx = time.Now().Unix() //nolint:wsl
|
||||||
|
|
||||||
|
// Похоже на понг второй стадии xep-0410 MUC-Ping-а, который у нас не реализован.
|
||||||
|
case v.To == j.Talk.JID() && string(v.Query) == "<XMLElement></XMLElement>":
|
||||||
|
mucNameMatch := slices.Contains(j.RoomsConnected, v.From)
|
||||||
|
|
||||||
|
if mucNameMatch {
|
||||||
|
log.Debugf("Got server-optimized MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
||||||
|
} else {
|
||||||
|
mucNickMatch := false
|
||||||
|
|
||||||
|
for _, room := range j.RoomsConnected {
|
||||||
|
mucNick := fmt.Sprintf("%s/%s", room, j.GetBotNickFromRoomConfig(room))
|
||||||
|
|
||||||
|
if v.From == mucNick {
|
||||||
|
mucNickMatch = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mucNickMatch {
|
||||||
|
log.Debugf("Got 2-nd stage MUC pong answer (xep-0410) from %s to %s", v.From, v.To)
|
||||||
|
} else {
|
||||||
|
// Поскольку никакой логики у нас на этот счёт не предусмотрено, то просто пропускаем ответ.
|
||||||
|
log.Infof("Got unknown pong from %s to %s", v.From, v.To)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подтверждение бана.
|
||||||
|
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)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Got strange ban successful message from %s to %s", v.From, v.To)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof("Got strange ban successful message from %s to %s", v.From, v.To)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ответ с результатом адресован нам.
|
||||||
|
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: name=%s version=%s",
|
||||||
|
v.From,
|
||||||
|
softwareVersion.Name,
|
||||||
|
softwareVersion.Version,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Infof(
|
||||||
|
"Recieved software version query result for %s: name=%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")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Info("Got an IQ result. Dunno how deal with it, discarding")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Этот бот не управляется со стороны сервера, поэтому все попытки порулить игнорируем.
|
||||||
|
case xmpp.IQTypeSet:
|
||||||
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Got an IQ request for set something. Answer not implemented")
|
||||||
|
|
||||||
|
if id, err := j.Talk.ErrorNotImplemented(
|
||||||
|
v,
|
||||||
|
"http://jabber.org/protocol/commands",
|
||||||
|
xmpp.IQTypeSet,
|
||||||
|
); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"unable to send set feature unimplemented to jabber server: id=%s, err=%w",
|
||||||
|
id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
// Нам прилетело сообщение об ошибке.
|
||||||
|
case xmpp.IQTypeError:
|
||||||
|
// Если сервер не хочет пинговаться и отвечает ошибкой на пинг, то наверно он не умеет в пинги,
|
||||||
|
// хотя если мы его пингуем, значит он анонсировал такой capability. Вот, засранец!
|
||||||
|
var (
|
||||||
|
iqPingStruct IqPing
|
||||||
|
parseError = true
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &iqPingStruct); err == nil {
|
||||||
|
if iqPingStruct.Xmlns == "urn:xmpp:ping" {
|
||||||
|
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 += "fallback to keepalive whitespace pings"
|
||||||
|
log.Error(msg)
|
||||||
|
|
||||||
|
j.ServerCapsList.Set("urn:xmpp:ping", false)
|
||||||
|
} else {
|
||||||
|
log.Errorf("Got 'ping unsupported' message from: %s to: %s", v.From, v.To)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error("Iq parsed as ping, but does not belong to xmlns urn:xmpp:ping")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Error(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Это у нас пинг xep-0410 и мы не в комнате, предполагается, что надо бы заджойниться.
|
||||||
|
if parseError {
|
||||||
|
var iqErrorCancelNotAcceptable IqErrorCancelNotAcceptable
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &iqErrorCancelNotAcceptable); err == nil {
|
||||||
|
if v.To == j.Talk.JID() {
|
||||||
|
nick := strings.SplitN(v.From, "/", 2)[1]
|
||||||
|
room := strings.SplitN(v.From, "/", 2)[0]
|
||||||
|
|
||||||
|
if slices.Contains(j.RoomsConnected, iqErrorCancelNotAcceptable.By) &&
|
||||||
|
nick == j.GetBotNickFromRoomConfig(room) {
|
||||||
|
log.Errorf(
|
||||||
|
"Got Iq error message from: %s to: %s. Looks like i'm not in MUC anymore",
|
||||||
|
v.From, v.To,
|
||||||
|
)
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(j.C.Jabber.MucRejoinDelay) * time.Second)
|
||||||
|
|
||||||
|
if _, err := j.Talk.JoinMUCNoHistory(iqErrorCancelNotAcceptable.By, nick); err != nil {
|
||||||
|
err := fmt.Errorf(
|
||||||
|
"looks like connection to server also lost err=%w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
j.GTomb.Kill(err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error(
|
||||||
|
"looks like message parsed as jabberIqErrorCancelNotAcceptable but we're not in given room",
|
||||||
|
)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error(
|
||||||
|
"Looks like message parsed as jabberIqErrorCancelNotAcceptable but addressed not to us",
|
||||||
|
)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parseError {
|
||||||
|
var IqErrorCancelServiceNotAvailable IqErrorCancelServiceNotAvailable
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &IqErrorCancelServiceNotAvailable); err == nil {
|
||||||
|
if v.To == j.Talk.JID() {
|
||||||
|
log.Infof("Jid %s disabled response to iq.version(?) request", v.From)
|
||||||
|
} else {
|
||||||
|
log.Error(
|
||||||
|
"Looks like message parsed as jabberIqErrorCancelServiceNotAvailable but addressed not to us",
|
||||||
|
)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
<iq from='slackware-current@conference.jabber.ru/Batman46(new)' to='buny@jabber.ru/Лунная призма'
|
||||||
|
type='error' id='94e55122-6c15-4e88-a0df-ae9f02e0b550'>
|
||||||
|
<query xmlns='jabber:iq:version'/>
|
||||||
|
<error code='404' type='cancel'>
|
||||||
|
<item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Адресата нет в конференции</text>
|
||||||
|
</error>
|
||||||
|
</iq>
|
||||||
|
*/
|
||||||
|
if parseError {
|
||||||
|
var IqErrorCancelItemNotFound IqErrorCancelItemNotFound
|
||||||
|
|
||||||
|
if err := xml.Unmarshal(v.Query, &IqErrorCancelItemNotFound); err == nil {
|
||||||
|
if v.To == j.Talk.JID() {
|
||||||
|
log.Infof("Jid %s disabled response to iq.version(?) request, hello batman46(new)", v.From)
|
||||||
|
} else {
|
||||||
|
log.Error(
|
||||||
|
"Looks like message parsed as jabberIqErrorCancelServiceNotAvailable but addressed not to us",
|
||||||
|
)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseError = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parseError {
|
||||||
|
log.Error("Unhandled IQ Error message")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Нам прилетело что-то неизвестное из семейства IQ stanza.
|
||||||
|
default:
|
||||||
|
log.Info("Got an unknown IQ request. Dunno how deal with it, discarding")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Смена статуса участника.
|
||||||
|
case xmpp.Presence:
|
||||||
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Type == xmpp.IQTypeError {
|
||||||
|
// Это событие происходит, когда из чятика выходит другой инстанс клиента.
|
||||||
|
// Такое бывает, когда 2 инстанса с одинаковым ресурсом, например, начинают "драться" за возможность
|
||||||
|
// остаться на сервере. Ситуация, которую допускать нельзя, на самом деле, потому что рано или поздно такого
|
||||||
|
// клиента забанят из-за спама "вошёл-вышел".
|
||||||
|
// Однако, это не значит, что такую ситуацию мы не должны корректным образом обрабатывать.
|
||||||
|
if v.Type == "unavailable" {
|
||||||
|
// Считаем, что мы больше не в комнате, поэтому не знаем, кто там есть.
|
||||||
|
j.RoomPresences.Delete(v.From)
|
||||||
|
|
||||||
|
log.Error("Presence notification - looks like another instance of client leaves room")
|
||||||
|
|
||||||
|
if slices.Contains(j.RoomsConnected, v.From) {
|
||||||
|
j.GTomb.Go(
|
||||||
|
func() error {
|
||||||
|
if err := j.JoinMuc(v.From); err != nil {
|
||||||
|
return fmt.Errorf("unable to join MUC %s: %w", v.From, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Errorf("Presence notification, Type: %s, From: %s, Show: %s, Status: %s",
|
||||||
|
v.Type, v.From, v.Show, v.Status)
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Error(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Infof(
|
||||||
|
"Presence notification, Type: %s, From: %s, To: %s Show: %s, Status: %s, Affiliation: %s, Role: %s, JID: %s, Priority: %s",
|
||||||
|
v.Type, v.From, v.To, v.Show, v.Status, v.Affiliation, v.Role, v.JID, v.Priority,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !strings.Contains(v.From, "/") {
|
||||||
|
log.Debugf("Looks like we have got Presence notification from roster: %s, skip it", v.From)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.JID == "" {
|
||||||
|
log.Info("Useless presence notification without JID field, skipping")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// На всякий случай добавим комнату, из которой пришло уведомление в список тех, в которые вхож бот.
|
||||||
|
if nick == j.GetBotNickFromRoomConfig(room) {
|
||||||
|
j.RoomsConnected = append(j.RoomsConnected, room)
|
||||||
|
// На всякий случай дедуплицируем список комнат, к которым мы заджойнились.
|
||||||
|
sort.Strings(j.RoomsConnected)
|
||||||
|
|
||||||
|
j.RoomsConnected = slices.Compact(j.RoomsConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Role {
|
||||||
|
// Участник ушёл.
|
||||||
|
case "none":
|
||||||
|
var (
|
||||||
|
newPresences = make([]xmpp.Presence, 0)
|
||||||
|
presenceJSON []byte
|
||||||
|
ok bool
|
||||||
|
presences []xmpp.Presence
|
||||||
|
)
|
||||||
|
|
||||||
|
// Обновлять базу имеет смысл только если есть хоть какая-то запись о комнате в ней.
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSON, ok = presenceJSONInterface.([]byte)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(presenceJSON, &presences); err != nil {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука.
|
||||||
|
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat)
|
||||||
|
// пропускаем его.
|
||||||
|
if p.From == v.From {
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Skipping presence for %s in room %s", v.JID, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Appending presence for %s in room %s", v.JID, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPresences = append(newPresences, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
presenceJSON, _ = json.Marshal(newPresences) //nolint: errchkjson
|
||||||
|
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Updating presence db for room %s", room)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.RoomPresences.Set(room, presenceJSON)
|
||||||
|
|
||||||
|
// Больше нам, похоже с этим уведомлением нечего.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Участник пришёл или изменил свой статус.
|
||||||
|
default:
|
||||||
|
var (
|
||||||
|
newPresences = make([]xmpp.Presence, 0)
|
||||||
|
presenceJSON []byte
|
||||||
|
ok bool
|
||||||
|
presences []xmpp.Presence
|
||||||
|
skip bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSON, ok = presenceJSONInterface.([]byte)
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы нашли запись о комнате, то её надо обновить.
|
||||||
|
if err := json.Unmarshal(presenceJSON, &presences); err != nil {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука.
|
||||||
|
// Если находим, что у нас уже есть клиент с таким же From, то есть полным nick-ом (для groupchat)
|
||||||
|
// запоминаем его и добавим позднее.
|
||||||
|
if p.From == v.From {
|
||||||
|
// Запоминаем участника только если его роль не поменялась на outcast. (То есть если это не
|
||||||
|
// бан.)
|
||||||
|
if v.Role == "outcast" {
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Let's forget about %s because it is outcast/banned", p.JID)
|
||||||
|
}
|
||||||
|
|
||||||
|
skip = true
|
||||||
|
} else if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Skip adding %s to presences for %s, add as last one", p.JID, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Appending presence for %s in room %s", v.JID, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPresences = append(newPresences, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skip {
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Appending presence for %s in room %s", v.JID, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPresences = append(newPresences, v)
|
||||||
|
} else if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Skip Appending outcast presence for %s in room %s", v.JID, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
presenceJSON, _ = json.Marshal(newPresences) //nolint: errchkjson
|
||||||
|
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("Updating presence db for room %s", room)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.RoomPresences.Set(room, presenceJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.C.Debug.PresenceDB {
|
||||||
|
log.Debugf("List of presence entries in presence db of %s:", room)
|
||||||
|
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
var presences []xmpp.Presence
|
||||||
|
|
||||||
|
presenceJSON, ok := presenceJSONInterface.([]byte)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы нашли запись о комнате, то её надо обновить.
|
||||||
|
if err := json.Unmarshal(presenceJSON, &presences); err != nil {
|
||||||
|
log.Debugf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука.
|
||||||
|
log.Debugf("Found presence for %s from %s", p.JID, room)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, а не злодей ли зашёл? Сделать это мы можем, только если мы находимся в комнате.
|
||||||
|
// По правилам, мы можем что-то делать, только после того, как нам прилетит наш собственный 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".
|
||||||
|
case xmpp.DiscoResult:
|
||||||
|
if muc, _ := strings.CutSuffix(v.To, "/"); muc != "" {
|
||||||
|
if slices.Contains(j.RoomsConnected, muc) {
|
||||||
|
j.LastMucActivity.Set(muc, j.LastServerActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Я видел 2 типа disco result и они отличались только []identities. Попробуем вытащить известный identity.
|
||||||
|
// TODO: проверять адресата v.To.
|
||||||
|
for _, ident := range v.Identities {
|
||||||
|
switch ident.Category {
|
||||||
|
case "server":
|
||||||
|
// Конкретно сейчас нас интересует только поддержка c2s ping.
|
||||||
|
for _, feature := range v.Features {
|
||||||
|
log.Debugf("Server %s announced that it supports feature: %s", v.From, feature)
|
||||||
|
j.ServerCapsList.Set(feature, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ServerCapsQueried = true
|
||||||
|
|
||||||
|
case "conference":
|
||||||
|
mucCaps := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, feature := range v.Features {
|
||||||
|
log.Debugf("MUC %s announced that it supports feature: %s", v.From, feature)
|
||||||
|
|
||||||
|
mucCaps[feature] = true //nolint:wsl
|
||||||
|
}
|
||||||
|
|
||||||
|
j.MucCapsList.Set(v.From, mucCaps)
|
||||||
|
|
||||||
|
case "pubsub":
|
||||||
|
log.Debugf("PubSub component %s reply to disco#info, skipping", v.From)
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Debug("Got unknown reply to disco#info")
|
||||||
|
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Debug(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Это что-то неизвестное, подампим событие в лог.
|
||||||
|
default:
|
||||||
|
for str := range strings.SplitSeq(spew.Sdump(e), "\n") {
|
||||||
|
log.Info(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
75
internal/jabber/main_loop.go
Normal file
75
internal/jabber/main_loop.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.Error())
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
j.ServerPingTimestampRx = time.Now().Unix() // Считаем, что если коннект запустился, то первый пинг успешен.
|
||||||
|
|
||||||
|
// Тыкаем сервер палочкой, проверяем, что коннект жив и вываливаемся из mainLoop, если он не жив.
|
||||||
|
j.GTomb.Go(
|
||||||
|
func() error {
|
||||||
|
err := j.ProbeServerLiveness()
|
||||||
|
log.Error(err.Error())
|
||||||
|
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Тыкаем muc-и палочкой, проверяем, что они живы и вываливаемся из mainLoop, если пинги пропали.
|
||||||
|
// Если пинги до комнаты пропали, то это фактически значит, что либо сервер потерял связь с MUC-компонентом,
|
||||||
|
// либо у нас какой-то wire error.
|
||||||
|
// j.GTomb.Go() тут зовётся внутри на каждую комнату, если у нас ошибка, эта горутинка рано или поздно выйдет.
|
||||||
|
go j.ProbeMUCLiveness()
|
||||||
|
|
||||||
|
// Гребём ивенты, пока стриггерилось завершение работы приложения, или соединение не порвалось.
|
||||||
|
for !j.Shutdown {
|
||||||
|
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 (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
282
internal/jabber/read_config.go
Normal file
282
internal/jabber/read_config.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hjson/hjson-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 := filepath.Dir(executablePath) + "/data/config.json"
|
||||||
|
|
||||||
|
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]any
|
||||||
|
)
|
||||||
|
|
||||||
|
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.DebugLib = false
|
||||||
|
// 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: */
|
||||||
64
internal/jabber/sig_handler.go
Normal file
64
internal/jabber/sig_handler.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SigHandler срабатывает если происходит завершение программы, инициированное либо init-ом, либо пользователем.
|
||||||
|
// Обрабатвает сигналы INT, TERM, QUIT.
|
||||||
|
func (j *Jabber) SigHandler() error {
|
||||||
|
log.Debug("Installing signal handler")
|
||||||
|
|
||||||
|
for s := range j.SigChan {
|
||||||
|
switch s {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
log.Info("Got SIGINT, quitting")
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
log.Info("Got SIGTERM, quitting")
|
||||||
|
case syscall.SIGQUIT:
|
||||||
|
log.Info("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.Info("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: */
|
||||||
81
internal/jabber/squash.go
Normal file
81
internal/jabber/squash.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xmppo/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: */
|
||||||
267
internal/jabber/types.go
Normal file
267
internal/jabber/types.go
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
"gopkg.in/tomb.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// MyConfig прототип структурки с конфигом.
|
||||||
|
MyConfig struct {
|
||||||
|
CSign string `json:"csign,omitempty"`
|
||||||
|
Loglevel string `json:"loglevel,omitempty"`
|
||||||
|
Log string `json:"log,omitempty"`
|
||||||
|
Debug struct {
|
||||||
|
Lib bool `json:"lib,omitempty"`
|
||||||
|
Ping bool `json:"ping,omitempty"`
|
||||||
|
PresenceDB bool `json:"presence_db,omitempty"`
|
||||||
|
} `json:"debug"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
ExeName string `json:"exe_name,omitempty"`
|
||||||
|
Jabber struct {
|
||||||
|
Server string `json:"server,omitempty"`
|
||||||
|
Nick string `json:"nick,omitempty"`
|
||||||
|
Resource string `json:"resource,omitempty"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
StartupStatus []string `json:"startup_status,omitempty"`
|
||||||
|
BotMasters []string `json:"bot_masters,omitempty"`
|
||||||
|
BanPhrases []string `json:"ban_phrases,omitempty"`
|
||||||
|
RuntimeStatus struct {
|
||||||
|
Text []string `json:"text,omitempty"`
|
||||||
|
RotationTime int64 `json:"rotation_time,omitempty"`
|
||||||
|
RotationSplayTime int64 `json:"rotation_splay_time,omitempty"`
|
||||||
|
} `json:"runtime_status"`
|
||||||
|
Channels []struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Nick string `json:"nick,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
AllCaps struct {
|
||||||
|
DefaultAction string `json:"default_action,omitempty"`
|
||||||
|
MinLength int `json:"min_length,omitempty"`
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
} `json:"all_caps"`
|
||||||
|
Bayes struct {
|
||||||
|
DefaultAction string `json:"default_action,omitempty"`
|
||||||
|
MinWords int64 `json:"min_words,omitempty"`
|
||||||
|
MinLength int `json:"min_length,omitempty"`
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
} `json:"bayes"`
|
||||||
|
} `json:"channels"`
|
||||||
|
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"`
|
||||||
|
BanDelay int64 `json:"ban_delay,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"`
|
||||||
|
BanPhrasesEnable bool `json:"ban_phrases_enable,omitempty"`
|
||||||
|
} `json:"jabber"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyWhiteList прототип структурки с белым списком jid-ов.
|
||||||
|
MyWhiteList struct {
|
||||||
|
Whitelist []struct {
|
||||||
|
RoomName string `json:"room_name"`
|
||||||
|
Jid []string `json:"jid,omitempty"`
|
||||||
|
WipeBans bool `json:"wipe_bans,omitempty"`
|
||||||
|
} `json:"whitelist,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyBlackList прототип структурки с чёрным списком jid-ов.
|
||||||
|
MyBlackList struct {
|
||||||
|
Blacklist []MyBlackListEntry `json:"blacklist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyBlackListEntry вложенная структура для MyBlackList.
|
||||||
|
MyBlackListEntry struct {
|
||||||
|
RoomName string `json:"room_name"`
|
||||||
|
JidRe []string `json:"jid_re"`
|
||||||
|
NickRe []string `json:"nick_re"`
|
||||||
|
PhraseRe []string `json:"phrase_re"`
|
||||||
|
UserAgent []MyBlackListEntryUserAgent `json:"user_agent"`
|
||||||
|
ReasonEnable bool `json:"reason_enable,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MyBlackListEntryUserAgent вложенная структура для MyBlackListEntry.
|
||||||
|
MyBlackListEntryUserAgent struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Os string `json:"os"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jabber основная структура-объект, содержащая стейты и проч.
|
||||||
|
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 .
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IqErrorCancelServiceNotAvailable обычно возвращается сервером в ответ на iq-запрос к несуществующей сущности.
|
||||||
|
// Если мы такое получаем от клиента, значит мы запросили что-то, что клиент явно не объявил как существущее в
|
||||||
|
// списке своих capability. https://xmpp.org/extensions/xep-0030.html#errors
|
||||||
|
IqErrorCancelServiceNotAvailable struct {
|
||||||
|
XMLName xml.Name `xml:"error"`
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
ServiceUnavailable struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <iq from='slackware-current@conference.jabber.ru/Batman46(new)' to='buny@jabber.ru/Лунная призма' type='error' id='94e55122-6
|
||||||
|
c15-4e88-a0df-ae9f02e0b550'>
|
||||||
|
<query xmlns='jabber:iq:version'/>
|
||||||
|
<error code='404' type='cancel'>
|
||||||
|
<item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>Адресата нет в конференции</text>
|
||||||
|
</error>
|
||||||
|
</iq>
|
||||||
|
*/
|
||||||
|
|
||||||
|
// IqErrorCancelItemNotFound не знаю, насколько корректно так отвечать на запрос версии ПО, но такое иногда
|
||||||
|
// встречаетсяю. Придётся поддержать.
|
||||||
|
IqErrorCancelItemNotFound struct {
|
||||||
|
XMLName xml.Name `xml:"error"`
|
||||||
|
Chardata string `xml:",chardata"`
|
||||||
|
Code string `xml:"code,attr"`
|
||||||
|
Type string `xml:"type,attr"`
|
||||||
|
ItemNotFound struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
} `xml:"item-not-found"`
|
||||||
|
Text struct {
|
||||||
|
Text string `xml:",chardata"`
|
||||||
|
Xmlns string `xml:"xmlns,attr"`
|
||||||
|
} `xml:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IqResultSoftwareVersion прототип структурки для разбора IQ ответов на запрос о названии и версии клиентского ПО.
|
||||||
|
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: */
|
||||||
466
internal/jabber/util.go
Normal file
466
internal/jabber/util.go
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
package jabber
|
||||||
|
|
||||||
|
import (
|
||||||
|
"buny-jabber-bot/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/xmppo/go-xmpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RandomPhrase выдаёт одну рандомную фразу из даденного списка фраз.
|
||||||
|
func RandomPhrase(list []string) string {
|
||||||
|
phrase := ""
|
||||||
|
|
||||||
|
if listLen := len(list); listLen > 0 {
|
||||||
|
phrase = list[rand.Intn(listLen)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return phrase
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceToStringSlice превращает данный интерфейс в слайс строк.
|
||||||
|
// Если может, конечно :) .
|
||||||
|
func InterfaceToStringSlice(iface any) []string {
|
||||||
|
var mySlice []string
|
||||||
|
|
||||||
|
// А теперь мы начинаем дурдом, нам надо превратить ёбанный any в []string.
|
||||||
|
// Поскольку any может быть чем угодно, перестрахуемся.
|
||||||
|
if reflect.TypeOf(iface).Kind() == reflect.Slice {
|
||||||
|
shit := reflect.ValueOf(iface)
|
||||||
|
|
||||||
|
for v := range shit.Seq() {
|
||||||
|
mySlice = append(mySlice, v.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mySlice
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRealJIDfromNick достаёт из запомненных presence-ов по даденному nick-у real jid с resource-ом. Nick должен
|
||||||
|
// содержать имя конфы, откуда участник.
|
||||||
|
func (j *Jabber) GetRealJIDfromNick(fullNick string) string {
|
||||||
|
var (
|
||||||
|
presenceJSON []byte
|
||||||
|
ok bool
|
||||||
|
presences []xmpp.Presence
|
||||||
|
)
|
||||||
|
|
||||||
|
room := (strings.SplitN(fullNick, "/", 2))[0]
|
||||||
|
|
||||||
|
if presenceJSONInterface, present := j.RoomPresences.Get(room); present {
|
||||||
|
presenceJSON, ok = presenceJSONInterface.([]byte)
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(presenceJSON, &presences); err != nil {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука.
|
||||||
|
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()); err != nil {
|
||||||
|
return fmt.Errorf("unable to send disco#info to MUC %s: %w", room, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ждём, пока muc нам вернёт список фичей.
|
||||||
|
for range 20 * int(j.C.Jabber.ConnectionTimeout) {
|
||||||
|
var (
|
||||||
|
myRoom any
|
||||||
|
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 range 20 * int(j.C.Jabber.ConnectionTimeout) {
|
||||||
|
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) })
|
||||||
|
|
||||||
|
// Время проверить участников на предмет злобности.
|
||||||
|
presenceJSONInterface, present := j.RoomPresences.Get(room)
|
||||||
|
|
||||||
|
if present {
|
||||||
|
presenceJSON, ok := presenceJSONInterface.([]byte)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var presences []xmpp.Presence
|
||||||
|
|
||||||
|
if err := json.Unmarshal(presenceJSON, &presences); err != nil {
|
||||||
|
log.Errorf("Looks like we have corrupted entry in presence db")
|
||||||
|
|
||||||
|
return nil //nolint: nilerr // If presence db is corrupted it is a bad thing, but we log it anyway.
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range presences { //nolint: gocritic // Развели тут, блять, страну советов, напиши что надо сделать кодом, сука.
|
||||||
|
log.Infof("Fake presence forged for %s just for on-enter check", p.JID)
|
||||||
|
// Оно там внутри всё само обработает, если вдруг возникнет wire error, то зарекконетится.
|
||||||
|
_ = j.BunyPresense(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
|
||||||
|
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 j.ServerCapsQueried { // Сервер ответил на disco#info.
|
||||||
|
var (
|
||||||
|
value any
|
||||||
|
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:
|
||||||
|
if j.C.Debug.Ping {
|
||||||
|
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 { // Первая пуля пока не вылетела, отправляем.
|
||||||
|
if j.C.Debug.Ping {
|
||||||
|
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:
|
||||||
|
if j.C.Debug.Ping {
|
||||||
|
log.Debug("Sending keepalive whitespace ping")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := j.Talk.SendKeepAlive(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // Сервер не ответил на disco#info.
|
||||||
|
if j.C.Debug.Ping {
|
||||||
|
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 any
|
||||||
|
)
|
||||||
|
|
||||||
|
// Если записи про комнату нету, то пинговать её бессмысленно.
|
||||||
|
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 any
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if j.C.Debug.Ping {
|
||||||
|
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: */
|
||||||
232
internal/log/log.go
Normal file
232
internal/log/log.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
// Package log wraps log/slog for more easier and convinient use.
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Handler exposes slog handler struct.
|
||||||
|
Handler slog.Handler
|
||||||
|
|
||||||
|
// Ctx log context. For now it is just sits here and do nothing.
|
||||||
|
Ctx = context.Background()
|
||||||
|
|
||||||
|
// Writer go-writer eported for capturing other types of logs, even for std log go facility.
|
||||||
|
Writer io.Writer
|
||||||
|
|
||||||
|
// reader is required for Writer to work properly. It is connected via io.Pipe() to Writer.
|
||||||
|
reader io.Reader
|
||||||
|
|
||||||
|
// Logger *log.Logger thing required for compatibility with old-good log package.
|
||||||
|
Logger log.Logger
|
||||||
|
|
||||||
|
// LogFileHandler is currently opened log file descriptor. Stderr by default.
|
||||||
|
LogFileHandler = os.Stderr
|
||||||
|
|
||||||
|
// LogLevel current loglevel. Info by default.
|
||||||
|
LogLevel = slog.LevelInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init setup logger stuff.
|
||||||
|
// level can be error, warn, info, debug if something other supplied info level selected.
|
||||||
|
// fileDescriptor should be opened before supplying it to Init().
|
||||||
|
func Init(level string, fileDescriptor *os.File) {
|
||||||
|
LogFileHandler = fileDescriptor
|
||||||
|
|
||||||
|
// no panic, no trace.
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
LogLevel = slog.LevelDebug
|
||||||
|
|
||||||
|
case "info":
|
||||||
|
LogLevel = slog.LevelInfo
|
||||||
|
|
||||||
|
case "warn":
|
||||||
|
LogLevel = slog.LevelWarn
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
LogLevel = slog.LevelError
|
||||||
|
|
||||||
|
default:
|
||||||
|
LogLevel = slog.LevelInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &slog.HandlerOptions{ //nolint: exhaustruct
|
||||||
|
// Use the ReplaceAttr function on the handler options
|
||||||
|
// to be able to replace any single attribute in the log output.
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive
|
||||||
|
// Check that we are handling the time key.
|
||||||
|
if a.Key != slog.TimeKey {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
t := a.Value.Time()
|
||||||
|
|
||||||
|
// change the value from a time.Time to a String
|
||||||
|
// where the string has the correct time format.
|
||||||
|
a.Value = slog.StringValue(t.Format(time.DateTime))
|
||||||
|
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
|
||||||
|
Level: LogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Documentation says that we can use *os.File in place of io.Writer, but in this case it did not work.
|
||||||
|
// So we have to cast fileDescriptor to io.Writer.
|
||||||
|
w := io.Writer(fileDescriptor)
|
||||||
|
|
||||||
|
Handler = slog.NewTextHandler(w, opts)
|
||||||
|
|
||||||
|
slog.SetDefault(
|
||||||
|
slog.New(Handler),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Catch log.Logger message at info loglevel of slog logging facility.
|
||||||
|
Logger = *slog.NewLogLogger(Handler, slog.LevelError)
|
||||||
|
|
||||||
|
reader, Writer = io.Pipe()
|
||||||
|
|
||||||
|
go readIoPipe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// readIoPipe reads pipe and writes it to slog.
|
||||||
|
func readIoPipe() {
|
||||||
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
|
for {
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text() // Get the current line as a string.
|
||||||
|
slog.Debug(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
// io.EOF is expected when done reading, e.g. on exit.
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// something weird happen, we should it log to guess later what it was.
|
||||||
|
Errorf("Error during scanning log writer pipe: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLevel returns current loglevel as string.
|
||||||
|
func GetLevel() string {
|
||||||
|
switch {
|
||||||
|
case Handler.Enabled(Ctx, slog.LevelDebug):
|
||||||
|
return "debug"
|
||||||
|
|
||||||
|
case Handler.Enabled(Ctx, slog.LevelInfo):
|
||||||
|
return "info"
|
||||||
|
|
||||||
|
case Handler.Enabled(Ctx, slog.LevelWarn):
|
||||||
|
return "warn"
|
||||||
|
|
||||||
|
case Handler.Enabled(Ctx, slog.LevelError):
|
||||||
|
return "error"
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal passes message directly to slog.Error() and exits with error code 1.
|
||||||
|
func Fatal(message string) {
|
||||||
|
slog.Error(message)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatalf logs message on error log level and exits with error code 1. It allows to supply arguments in printf() style .
|
||||||
|
func Fatalf(format string, a ...any) {
|
||||||
|
slog.Error(fmt.Sprintf(format, a...))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error passes message directly to slog.Error().
|
||||||
|
func Error(message string) {
|
||||||
|
slog.Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf logs message on error log level and allows to supply arguments in printf() style.
|
||||||
|
func Errorf(format string, a ...any) {
|
||||||
|
slog.Error(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn passes message directly to slog.Warn().
|
||||||
|
func Warn(message string) {
|
||||||
|
slog.Warn(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnf logs message on warn log level and allows to supply arguments in printf() style.
|
||||||
|
func Warnf(format string, a ...any) {
|
||||||
|
slog.Warn(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info passes message directly to slog.Info().
|
||||||
|
func Info(message string) {
|
||||||
|
slog.Info(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infof logs message on info log level and allows to supply arguments in printf() style.
|
||||||
|
func Infof(format string, a ...any) {
|
||||||
|
slog.Info(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug passes message directly to slog.Debug().
|
||||||
|
func Debug(message string) {
|
||||||
|
slog.Debug(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugf logs message on debug log level and allows to supply arguments in printf() style.
|
||||||
|
func Debugf(format string, a ...any) {
|
||||||
|
slog.Debug(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes opened log file descriptors.
|
||||||
|
func Close() {
|
||||||
|
LogFileHandler.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReOpenLog closes and opens log file again.
|
||||||
|
func ReOpenLog() {
|
||||||
|
filename := LogFileHandler.Name()
|
||||||
|
|
||||||
|
// If we writing to stderr, we should not re-open it.
|
||||||
|
if filename == os.Stderr.Name() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loglevel := GetLevel()
|
||||||
|
|
||||||
|
LogFileHandler.Close()
|
||||||
|
|
||||||
|
file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Try to emulate our own log message format.
|
||||||
|
fmt.Fprintf(
|
||||||
|
os.Stderr,
|
||||||
|
"time=\"%s\" level=ERROR msg=\"Unable to open logfile %s: %s\"",
|
||||||
|
time.Now().Format(time.DateTime),
|
||||||
|
filename,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Init(loglevel, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
921
lib.go
921
lib.go
@@ -1,921 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"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() 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{
|
|
||||||
"~/.bunyPresense-jabber-bot.json",
|
|
||||||
"~/bunyPresense-jabber-bot.json",
|
|
||||||
"/etc/bunyPresense-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 == "" {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если список фраз с которыми стартует бот пустой, вносим в него 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
|
|
||||||
|
|
||||||
config = sampleConfig
|
|
||||||
configLoaded = true
|
|
||||||
log.Infof("Using %s as config file", location) //nolint:wsl
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !configLoaded {
|
|
||||||
return errors.New("config was not loaded!") //nolint:goerr113
|
|
||||||
}
|
|
||||||
|
|
||||||
return err //nolint:wrapcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// readWhitelist читает и валидирует белые списки пользователей.
|
|
||||||
func 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{
|
|
||||||
"~/.bunyPresense-jabber-bot-whitelist.json",
|
|
||||||
"~/bunyPresense-jabber-bot-whitelist.json",
|
|
||||||
"/etc/bunyPresense-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 {
|
|
||||||
return errors.New("whitelist was not loaded!") //nolint:goerr113
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBlacklist читает и валидирует чёрные списки пользователей.
|
|
||||||
func 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
|
|
||||||
}
|
|
||||||
|
|
||||||
blackList = sampleBlacklist
|
|
||||||
blacklistLoaded = true
|
|
||||||
|
|
||||||
log.Infof("Using %s as blacklist file", location)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !blacklistLoaded {
|
|
||||||
return errors.New("blacklist was not loaded!")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ждём, пока 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)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Время проверить участников на предмет злобности
|
|
||||||
namesInterface, present := 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, то зарекконетится.
|
|
||||||
_ = bunyPresense(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
// По-умолчанию, мы отправляем 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)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serverPingTimestampTx = time.Now().Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сервер не анонсировал, что умеет в c2s пинги
|
|
||||||
default:
|
|
||||||
log.Debug("Sending keepalive whitespace ping")
|
|
||||||
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else { // Сервер не ответил на disco#info
|
|
||||||
log.Debug("Sending keepalive whitespace ping")
|
|
||||||
|
|
||||||
if _, err := talk.SendKeepAlive(); err != nil {
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// probeMUCLiveness Пингует MUC-и, нужно для проверки, что клиент ещё находится в MUC-е.
|
|
||||||
func probeMUCLiveness() { //nolint:gocognit
|
|
||||||
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)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(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 {
|
|
||||||
err := fmt.Errorf("unable to ping MUC %s: %w", room, err)
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 getRealJIDfromNick(fullNick string) string {
|
|
||||||
var p xmpp.Presence
|
|
||||||
|
|
||||||
room := (strings.SplitN(fullNick, "/", 2))[0]
|
|
||||||
|
|
||||||
// Достанем presence участника
|
|
||||||
presenceJSONInterface, present := 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 ""
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
207
main.go
207
main.go
@@ -1,207 +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",
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := readConfig(); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Самое время вгрузить белые и чёрные списки, если что - получим ошибку в лог.
|
|
||||||
if err := readWhitelist(); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := 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)
|
|
||||||
|
|
||||||
// Хэндлер сигналов не надо трогать, он нужен для завершения программы целиком
|
|
||||||
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
|
|
||||||
roomPresences = NewCollection()
|
|
||||||
|
|
||||||
// Установим коннект
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Попробуем скрасить печальные будни generic-ошибок...
|
|
||||||
switch {
|
|
||||||
// Стрим не читается, он закрылся с той стороны во время чтения
|
|
||||||
case errors.Is(err, io.EOF):
|
|
||||||
err = fmt.Errorf("tcp stream closed while reading, err=%w", err)
|
|
||||||
|
|
||||||
// Пытаемся читать закрытый сокет
|
|
||||||
case errors.Is(err, net.ErrClosed):
|
|
||||||
err = fmt.Errorf("unable to read closed socket, err=%w", err)
|
|
||||||
|
|
||||||
// Не смогли записать в сокет
|
|
||||||
case errors.Is(err, net.ErrWriteToConnected):
|
|
||||||
err = fmt.Errorf("unable to write to socket, err=%w", err)
|
|
||||||
|
|
||||||
// Не сетевая проблема
|
|
||||||
default:
|
|
||||||
// Это уже что-то странное.
|
|
||||||
// Вероятно, ошибка парсинга xml. Собственно, баг сервера, тут мы ничего поделать не можем
|
|
||||||
err = fmt.Errorf("error during parsing received message, err=%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gTomb.Kill(err)
|
|
||||||
|
|
||||||
// Не забываем выходить на ошибке :(
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parseEvent(chat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
114
types.go
114
types.go
@@ -1,114 +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"`
|
|
||||||
BotMasters []string `json:"bot_masters"`
|
|
||||||
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"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
} `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
66
vendor/github.com/eleksir/go-xmpp/.golangci.yml
generated
vendored
@@ -1,66 +0,0 @@
|
|||||||
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
|
|
||||||
30
vendor/github.com/eleksir/go-xmpp/LICENSE
generated
vendored
30
vendor/github.com/eleksir/go-xmpp/LICENSE
generated
vendored
@@ -1,30 +0,0 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
|
||||||
Copyright (c) 2012 Russ Cox <rsc@swtch.com>
|
|
||||||
Copyright (c) 2023 Yasuhiro Matsumoto <mattn.jp@gmail.com>
|
|
||||||
Copyright (c) 2023 Sergei Fedosoff <eleksir@gmail.com>
|
|
||||||
|
|
||||||
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.
|
|
||||||
14
vendor/github.com/eleksir/go-xmpp/README.md
generated
vendored
14
vendor/github.com/eleksir/go-xmpp/README.md
generated
vendored
@@ -1,14 +0,0 @@
|
|||||||
go-xmpp
|
|
||||||
=======
|
|
||||||
|
|
||||||
go xmpp library.
|
|
||||||
|
|
||||||
[Original](https://github.com/rsc/rsc/blob/master/xmpp/xmpp.go) work done by [Russ Cox](https://github.com/rsc),
|
|
||||||
then [Yasuhiro Matsumoto](https://github.com/mattn) fork it and continue to improve 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)
|
|
||||||
|
|
||||||
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. And of course fell free to for it if needed.
|
|
||||||
1805
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
1805
vendor/github.com/eleksir/go-xmpp/xmpp.go
generated
vendored
File diff suppressed because it is too large
Load Diff
125
vendor/github.com/eleksir/go-xmpp/xmpp_information_query.go
generated
vendored
125
vendor/github.com/eleksir/go-xmpp/xmpp_information_query.go
generated
vendored
@@ -1,125 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const IQTypeGet = "get"
|
|
||||||
const IQTypeSet = "set"
|
|
||||||
const IQTypeResult = "result"
|
|
||||||
const IQTypeError = "error"
|
|
||||||
|
|
||||||
func (c *Client) Discovery() (string, error) {
|
|
||||||
// use getCookie for a pseudo random id.
|
|
||||||
reqID := strconv.FormatUint(uint64(getCookie()), 10)
|
|
||||||
|
|
||||||
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.
|
|
||||||
func (c *Client) DiscoverNodeInfo(node string) (string, error) {
|
|
||||||
query := fmt.Sprintf("<query xmlns='%s' node='%s'/>", XMPPNS_DISCO_INFO, node)
|
|
||||||
|
|
||||||
return c.RawInformation(c.jid, c.domain, "info3", IQTypeGet, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscoverInfo discovers information about given item from given jid.
|
|
||||||
func (c *Client) DiscoverInfo(from string, to string) (string, error) {
|
|
||||||
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_INFO)
|
|
||||||
|
|
||||||
return c.RawInformation(from, to, "info3", IQTypeGet, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscoverServerItems discover items that the server exposes.
|
|
||||||
func (c *Client) DiscoverServerItems() (string, error) {
|
|
||||||
return c.DiscoverEntityItems(c.domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiscoverEntityItems discovers items that an entity exposes.
|
|
||||||
func (c *Client) DiscoverEntityItems(jid string) (string, error) {
|
|
||||||
query := fmt.Sprintf("<query xmlns='%s'/>", XMPPNS_DISCO_ITEMS)
|
|
||||||
|
|
||||||
return c.RawInformation(c.jid, jid, "info1", IQTypeGet, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawInformationQuery sends an information query request to the server.
|
|
||||||
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>"
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, requestNamespace, body)
|
|
||||||
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawInformation sends a IQ request with the payload body to the server.
|
|
||||||
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>"
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, xmlIQ, xmlEscape(from), xmlEscape(to), id, iqType, body)
|
|
||||||
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IqVersionResponse responding with software version, according to xep-0092.
|
|
||||||
func (c *Client) IqVersionResponse(v IQ, name, version, os string) (string, error) {
|
|
||||||
if name == "" {
|
|
||||||
name = "go-xmpp"
|
|
||||||
}
|
|
||||||
|
|
||||||
if version == "" {
|
|
||||||
version = "undefined"
|
|
||||||
}
|
|
||||||
|
|
||||||
query := "<query xmlns=\"jabber:iq:version\">"
|
|
||||||
query += fmt.Sprintf("<name>%s</name>", name)
|
|
||||||
query += fmt.Sprintf("<version>%s</version>", version)
|
|
||||||
|
|
||||||
if os != "" {
|
|
||||||
query += fmt.Sprintf("<os>%s</os>", os)
|
|
||||||
}
|
|
||||||
|
|
||||||
query += "</query>"
|
|
||||||
|
|
||||||
return c.RawInformation(
|
|
||||||
v.To,
|
|
||||||
v.From,
|
|
||||||
v.ID,
|
|
||||||
IQTypeResult,
|
|
||||||
query,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JabberIqLastResponse responding with relative time since last activity.
|
|
||||||
// Here lastActivity is unix time stamp when last activity took place since.
|
|
||||||
func (c *Client) JabberIqLastResponse(v IQ, lastActivity int64) (string, error) {
|
|
||||||
query := fmt.Sprintf(
|
|
||||||
"<query xmlns=\"jabber:iq:last\" seconds=\"%d\" />",
|
|
||||||
time.Now().Unix()-lastActivity,
|
|
||||||
)
|
|
||||||
|
|
||||||
return c.RawInformation(
|
|
||||||
v.To,
|
|
||||||
v.From,
|
|
||||||
v.ID,
|
|
||||||
IQTypeResult,
|
|
||||||
query,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UrnXMPPTimeResponse implements response to query entity's current time (xep-0202).
|
|
||||||
// TimezoneOffset have format +HH:MM or -HH:MM.
|
|
||||||
func (c *Client) UrnXMPPTimeResponse(v IQ, timezoneOffset string) (string, error) {
|
|
||||||
query := fmt.Sprintf(
|
|
||||||
"<time xmlns=\"urn:xmpp:time\"><tzo>%s</tzo><utc>%s</utc></time>",
|
|
||||||
timezoneOffset,
|
|
||||||
time.Now().UTC().Format(time.RFC3339),
|
|
||||||
)
|
|
||||||
|
|
||||||
return c.RawInformation(
|
|
||||||
v.To,
|
|
||||||
v.From,
|
|
||||||
v.ID,
|
|
||||||
IQTypeResult,
|
|
||||||
query,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
140
vendor/github.com/eleksir/go-xmpp/xmpp_muc.go
generated
vendored
140
vendor/github.com/eleksir/go-xmpp/xmpp_muc.go
generated
vendored
@@ -1,140 +0,0 @@
|
|||||||
// Copyright 2013 Flo Lauber <dev@qatfy.at>. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// TODO(flo):
|
|
||||||
// - cleanup signatures of join/leave functions
|
|
||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
nsMUC = "http://jabber.org/protocol/muc"
|
|
||||||
nsMUCUser = "http://jabber.org/protocol/muc#user"
|
|
||||||
NoHistory = 0
|
|
||||||
CharHistory = 1
|
|
||||||
StanzaHistory = 2
|
|
||||||
SecondsHistory = 3
|
|
||||||
SinceHistory = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
// SendTopic sends room topic wrapped inside an XMPP message stanza body.
|
|
||||||
func (c *Client) SendTopic(chat Chat) (int, error) {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoinMUCNoHistory joins room and instructs server that no history data required.
|
|
||||||
func (c *Client) JoinMUCNoHistory(jid, nick string) (int, error) {
|
|
||||||
if nick == "" {
|
|
||||||
nick = c.jid
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>"+
|
|
||||||
"<history maxchars='0'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoinMUC joins room, xep-0045 7.2.
|
|
||||||
func (c *Client) JoinMUC(jid, nick string, historyType, history int, historyDate *time.Time) (int, error) {
|
|
||||||
if nick == "" {
|
|
||||||
nick = c.jid
|
|
||||||
}
|
|
||||||
|
|
||||||
switch historyType {
|
|
||||||
case NoHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s' />\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC)
|
|
||||||
case CharHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<history maxchars='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
|
||||||
case StanzaHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<history maxstanzas='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
|
||||||
case SecondsHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<history seconds='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, history)
|
|
||||||
case SinceHistory:
|
|
||||||
if historyDate != nil {
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<history since='%s'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, historyDate.Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, errors.New("unknown history option")
|
|
||||||
}
|
|
||||||
|
|
||||||
// JoinProtectedMUC joins password protected room, xep-0045 7.2.6.
|
|
||||||
func (c *Client) JoinProtectedMUC(jid, nick string, password string, historyType, history int, historyDate *time.Time) (int, error) {
|
|
||||||
if nick == "" {
|
|
||||||
nick = c.jid
|
|
||||||
}
|
|
||||||
|
|
||||||
switch historyType {
|
|
||||||
case NoHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<password>%s</password>"+
|
|
||||||
"</x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password))
|
|
||||||
case CharHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history maxchars='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
|
||||||
case StanzaHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history maxstanzas='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
|
||||||
case SecondsHistory:
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history seconds='%d'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), history)
|
|
||||||
case SinceHistory:
|
|
||||||
if historyDate != nil {
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence to='%s/%s'>\n"+
|
|
||||||
"<x xmlns='%s'>\n"+
|
|
||||||
"<password>%s</password>\n"+
|
|
||||||
"<history since='%s'/></x>\n"+
|
|
||||||
"</presence>",
|
|
||||||
xmlEscape(jid), xmlEscape(nick), nsMUC, xmlEscape(password), historyDate.Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, errors.New("unknown history option")
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaveMUC quits from room xep-0045 7.14.
|
|
||||||
func (c *Client) LeaveMUC(jid string) (int, error) {
|
|
||||||
return fmt.Fprintf(StanzaWriter, "<presence from='%s' to='%s' type='unavailable' />",
|
|
||||||
c.jid, xmlEscape(jid))
|
|
||||||
}
|
|
||||||
49
vendor/github.com/eleksir/go-xmpp/xmpp_ping.go
generated
vendored
49
vendor/github.com/eleksir/go-xmpp/xmpp_ping.go
generated
vendored
@@ -1,49 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) PingC2S(jid, server string) error {
|
|
||||||
if jid == "" {
|
|
||||||
jid = c.jid
|
|
||||||
}
|
|
||||||
|
|
||||||
if server == "" {
|
|
||||||
server = c.domain
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<iq from='%s' to='%s' id='c2s1' type='get'>\n"+
|
|
||||||
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
|
||||||
"</iq>",
|
|
||||||
xmlEscape(jid), xmlEscape(server))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) PingS2S(fromServer, toServer string) error {
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<iq from='%s' to='%s' id='s2s1' type='get'>\n"+
|
|
||||||
"<ping xmlns='urn:xmpp:ping'/>\n"+
|
|
||||||
"</iq>",
|
|
||||||
xmlEscape(fromServer), xmlEscape(toServer))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SendResultPing(id, toServer string) error {
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<iq type='result' to='%s' id='%s'/>",
|
|
||||||
xmlEscape(toServer), xmlEscape(id))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingResponse responding to ping query according to xep-0199.
|
|
||||||
func (c *Client) PingResponse(v IQ) (string, error) {
|
|
||||||
return c.RawInformation(
|
|
||||||
v.To,
|
|
||||||
v.From,
|
|
||||||
v.ID,
|
|
||||||
IQTypeResult,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
33
vendor/github.com/eleksir/go-xmpp/xmpp_subscription.go
generated
vendored
33
vendor/github.com/eleksir/go-xmpp/xmpp_subscription.go
generated
vendored
@@ -1,33 +0,0 @@
|
|||||||
package xmpp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *Client) ApproveSubscription(jid string) error {
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<presence to='%s' type='subscribed'/>",
|
|
||||||
xmlEscape(jid))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RevokeSubscription(jid string) error {
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<presence to='%s' type='unsubscribed'/>",
|
|
||||||
xmlEscape(jid))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RetrieveSubscription(jid string) error {
|
|
||||||
_, err := fmt.Fprintf(c.conn, "<presence to='%s' type='unsubscribe'/>",
|
|
||||||
xmlEscape(jid))
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RequestSubscription(jid string) error {
|
|
||||||
_, err := fmt.Fprintf(StanzaWriter, "<presence to='%s' type='subscribe'/>",
|
|
||||||
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
|
||||||
2
vendor/golang.org/x/exp/LICENSE → vendor/github.com/google/uuid/LICENSE
generated
vendored
2
vendor/golang.org/x/exp/LICENSE → vendor/github.com/google/uuid/LICENSE
generated
vendored
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
Copyright (c) 2009,2014 Google Inc. All rights reserved.
|
||||||
|
|
||||||
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
|
||||||
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/github.com/sirupsen/logrus/.gitignore
generated
vendored
4
vendor/github.com/sirupsen/logrus/.gitignore
generated
vendored
@@ -1,4 +0,0 @@
|
|||||||
logrus
|
|
||||||
vendor
|
|
||||||
|
|
||||||
.idea/
|
|
||||||
40
vendor/github.com/sirupsen/logrus/.golangci.yml
generated
vendored
40
vendor/github.com/sirupsen/logrus/.golangci.yml
generated
vendored
@@ -1,40 +0,0 @@
|
|||||||
run:
|
|
||||||
# do not run on test files yet
|
|
||||||
tests: false
|
|
||||||
|
|
||||||
# all available settings of specific linters
|
|
||||||
linters-settings:
|
|
||||||
errcheck:
|
|
||||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
|
||||||
# default is false: such cases aren't reported by default.
|
|
||||||
check-type-assertions: false
|
|
||||||
|
|
||||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
|
||||||
# default is false: such cases aren't reported by default.
|
|
||||||
check-blank: false
|
|
||||||
|
|
||||||
lll:
|
|
||||||
line-length: 100
|
|
||||||
tab-width: 4
|
|
||||||
|
|
||||||
prealloc:
|
|
||||||
simple: false
|
|
||||||
range-loops: false
|
|
||||||
for-loops: false
|
|
||||||
|
|
||||||
whitespace:
|
|
||||||
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
|
|
||||||
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
|
|
||||||
|
|
||||||
linters:
|
|
||||||
enable:
|
|
||||||
- megacheck
|
|
||||||
- govet
|
|
||||||
disable:
|
|
||||||
- maligned
|
|
||||||
- prealloc
|
|
||||||
disable-all: false
|
|
||||||
presets:
|
|
||||||
- bugs
|
|
||||||
- unused
|
|
||||||
fast: false
|
|
||||||
15
vendor/github.com/sirupsen/logrus/.travis.yml
generated
vendored
15
vendor/github.com/sirupsen/logrus/.travis.yml
generated
vendored
@@ -1,15 +0,0 @@
|
|||||||
language: go
|
|
||||||
go_import_path: github.com/sirupsen/logrus
|
|
||||||
git:
|
|
||||||
depth: 1
|
|
||||||
env:
|
|
||||||
- GO111MODULE=on
|
|
||||||
go: 1.15.x
|
|
||||||
os: linux
|
|
||||||
install:
|
|
||||||
- ./travis/install.sh
|
|
||||||
script:
|
|
||||||
- cd ci
|
|
||||||
- go run mage.go -v -w ../ crossBuild
|
|
||||||
- go run mage.go -v -w ../ lint
|
|
||||||
- go run mage.go -v -w ../ test
|
|
||||||
259
vendor/github.com/sirupsen/logrus/CHANGELOG.md
generated
vendored
259
vendor/github.com/sirupsen/logrus/CHANGELOG.md
generated
vendored
@@ -1,259 +0,0 @@
|
|||||||
# 1.8.1
|
|
||||||
Code quality:
|
|
||||||
* move magefile in its own subdir/submodule to remove magefile dependency on logrus consumer
|
|
||||||
* improve timestamp format documentation
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
* fix race condition on logger hooks
|
|
||||||
|
|
||||||
|
|
||||||
# 1.8.0
|
|
||||||
|
|
||||||
Correct versioning number replacing v1.7.1.
|
|
||||||
|
|
||||||
# 1.7.1
|
|
||||||
|
|
||||||
Beware this release has introduced a new public API and its semver is therefore incorrect.
|
|
||||||
|
|
||||||
Code quality:
|
|
||||||
* use go 1.15 in travis
|
|
||||||
* use magefile as task runner
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
* small fixes about new go 1.13 error formatting system
|
|
||||||
* Fix for long time race condiction with mutating data hooks
|
|
||||||
|
|
||||||
Features:
|
|
||||||
* build support for zos
|
|
||||||
|
|
||||||
# 1.7.0
|
|
||||||
Fixes:
|
|
||||||
* the dependency toward a windows terminal library has been removed
|
|
||||||
|
|
||||||
Features:
|
|
||||||
* a new buffer pool management API has been added
|
|
||||||
* a set of `<LogLevel>Fn()` functions have been added
|
|
||||||
|
|
||||||
# 1.6.0
|
|
||||||
Fixes:
|
|
||||||
* end of line cleanup
|
|
||||||
* revert the entry concurrency bug fix whic leads to deadlock under some circumstances
|
|
||||||
* update dependency on go-windows-terminal-sequences to fix a crash with go 1.14
|
|
||||||
|
|
||||||
Features:
|
|
||||||
* add an option to the `TextFormatter` to completely disable fields quoting
|
|
||||||
|
|
||||||
# 1.5.0
|
|
||||||
Code quality:
|
|
||||||
* add golangci linter run on travis
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
* add mutex for hooks concurrent access on `Entry` data
|
|
||||||
* caller function field for go1.14
|
|
||||||
* fix build issue for gopherjs target
|
|
||||||
|
|
||||||
Feature:
|
|
||||||
* add an hooks/writer sub-package whose goal is to split output on different stream depending on the trace level
|
|
||||||
* add a `DisableHTMLEscape` option in the `JSONFormatter`
|
|
||||||
* add `ForceQuote` and `PadLevelText` options in the `TextFormatter`
|
|
||||||
|
|
||||||
# 1.4.2
|
|
||||||
* Fixes build break for plan9, nacl, solaris
|
|
||||||
# 1.4.1
|
|
||||||
This new release introduces:
|
|
||||||
* Enhance TextFormatter to not print caller information when they are empty (#944)
|
|
||||||
* Remove dependency on golang.org/x/crypto (#932, #943)
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
* Fix Entry.WithContext method to return a copy of the initial entry (#941)
|
|
||||||
|
|
||||||
# 1.4.0
|
|
||||||
This new release introduces:
|
|
||||||
* Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
|
|
||||||
* Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter` (#909, #911)
|
|
||||||
* Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
* Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
|
|
||||||
* Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
|
|
||||||
* Fix infinite recursion on unknown `Level.String()` (#907)
|
|
||||||
* Fix race condition in `getCaller` (#916).
|
|
||||||
|
|
||||||
|
|
||||||
# 1.3.0
|
|
||||||
This new release introduces:
|
|
||||||
* Log, Logf, Logln functions for Logger and Entry that take a Level
|
|
||||||
|
|
||||||
Fixes:
|
|
||||||
* Building prometheus node_exporter on AIX (#840)
|
|
||||||
* Race condition in TextFormatter (#468)
|
|
||||||
* Travis CI import path (#868)
|
|
||||||
* Remove coloured output on Windows (#862)
|
|
||||||
* Pointer to func as field in JSONFormatter (#870)
|
|
||||||
* Properly marshal Levels (#873)
|
|
||||||
|
|
||||||
# 1.2.0
|
|
||||||
This new release introduces:
|
|
||||||
* A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
|
|
||||||
* A new trace level named `Trace` whose level is below `Debug`
|
|
||||||
* A configurable exit function to be called upon a Fatal trace
|
|
||||||
* The `Level` object now implements `encoding.TextUnmarshaler` interface
|
|
||||||
|
|
||||||
# 1.1.1
|
|
||||||
This is a bug fix release.
|
|
||||||
* fix the build break on Solaris
|
|
||||||
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
|
|
||||||
|
|
||||||
# 1.1.0
|
|
||||||
This new release introduces:
|
|
||||||
* several fixes:
|
|
||||||
* a fix for a race condition on entry formatting
|
|
||||||
* proper cleanup of previously used entries before putting them back in the pool
|
|
||||||
* the extra new line at the end of message in text formatter has been removed
|
|
||||||
* a new global public API to check if a level is activated: IsLevelEnabled
|
|
||||||
* the following methods have been added to the Logger object
|
|
||||||
* IsLevelEnabled
|
|
||||||
* SetFormatter
|
|
||||||
* SetOutput
|
|
||||||
* ReplaceHooks
|
|
||||||
* introduction of go module
|
|
||||||
* an indent configuration for the json formatter
|
|
||||||
* output colour support for windows
|
|
||||||
* the field sort function is now configurable for text formatter
|
|
||||||
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
|
|
||||||
|
|
||||||
# 1.0.6
|
|
||||||
|
|
||||||
This new release introduces:
|
|
||||||
* a new api WithTime which allows to easily force the time of the log entry
|
|
||||||
which is mostly useful for logger wrapper
|
|
||||||
* a fix reverting the immutability of the entry given as parameter to the hooks
|
|
||||||
a new configuration field of the json formatter in order to put all the fields
|
|
||||||
in a nested dictionnary
|
|
||||||
* a new SetOutput method in the Logger
|
|
||||||
* a new configuration of the textformatter to configure the name of the default keys
|
|
||||||
* a new configuration of the text formatter to disable the level truncation
|
|
||||||
|
|
||||||
# 1.0.5
|
|
||||||
|
|
||||||
* Fix hooks race (#707)
|
|
||||||
* Fix panic deadlock (#695)
|
|
||||||
|
|
||||||
# 1.0.4
|
|
||||||
|
|
||||||
* Fix race when adding hooks (#612)
|
|
||||||
* Fix terminal check in AppEngine (#635)
|
|
||||||
|
|
||||||
# 1.0.3
|
|
||||||
|
|
||||||
* Replace example files with testable examples
|
|
||||||
|
|
||||||
# 1.0.2
|
|
||||||
|
|
||||||
* bug: quote non-string values in text formatter (#583)
|
|
||||||
* Make (*Logger) SetLevel a public method
|
|
||||||
|
|
||||||
# 1.0.1
|
|
||||||
|
|
||||||
* bug: fix escaping in text formatter (#575)
|
|
||||||
|
|
||||||
# 1.0.0
|
|
||||||
|
|
||||||
* Officially changed name to lower-case
|
|
||||||
* bug: colors on Windows 10 (#541)
|
|
||||||
* bug: fix race in accessing level (#512)
|
|
||||||
|
|
||||||
# 0.11.5
|
|
||||||
|
|
||||||
* feature: add writer and writerlevel to entry (#372)
|
|
||||||
|
|
||||||
# 0.11.4
|
|
||||||
|
|
||||||
* bug: fix undefined variable on solaris (#493)
|
|
||||||
|
|
||||||
# 0.11.3
|
|
||||||
|
|
||||||
* formatter: configure quoting of empty values (#484)
|
|
||||||
* formatter: configure quoting character (default is `"`) (#484)
|
|
||||||
* bug: fix not importing io correctly in non-linux environments (#481)
|
|
||||||
|
|
||||||
# 0.11.2
|
|
||||||
|
|
||||||
* bug: fix windows terminal detection (#476)
|
|
||||||
|
|
||||||
# 0.11.1
|
|
||||||
|
|
||||||
* bug: fix tty detection with custom out (#471)
|
|
||||||
|
|
||||||
# 0.11.0
|
|
||||||
|
|
||||||
* performance: Use bufferpool to allocate (#370)
|
|
||||||
* terminal: terminal detection for app-engine (#343)
|
|
||||||
* feature: exit handler (#375)
|
|
||||||
|
|
||||||
# 0.10.0
|
|
||||||
|
|
||||||
* feature: Add a test hook (#180)
|
|
||||||
* feature: `ParseLevel` is now case-insensitive (#326)
|
|
||||||
* feature: `FieldLogger` interface that generalizes `Logger` and `Entry` (#308)
|
|
||||||
* performance: avoid re-allocations on `WithFields` (#335)
|
|
||||||
|
|
||||||
# 0.9.0
|
|
||||||
|
|
||||||
* logrus/text_formatter: don't emit empty msg
|
|
||||||
* logrus/hooks/airbrake: move out of main repository
|
|
||||||
* logrus/hooks/sentry: move out of main repository
|
|
||||||
* logrus/hooks/papertrail: move out of main repository
|
|
||||||
* logrus/hooks/bugsnag: move out of main repository
|
|
||||||
* logrus/core: run tests with `-race`
|
|
||||||
* logrus/core: detect TTY based on `stderr`
|
|
||||||
* logrus/core: support `WithError` on logger
|
|
||||||
* logrus/core: Solaris support
|
|
||||||
|
|
||||||
# 0.8.7
|
|
||||||
|
|
||||||
* logrus/core: fix possible race (#216)
|
|
||||||
* logrus/doc: small typo fixes and doc improvements
|
|
||||||
|
|
||||||
|
|
||||||
# 0.8.6
|
|
||||||
|
|
||||||
* hooks/raven: allow passing an initialized client
|
|
||||||
|
|
||||||
# 0.8.5
|
|
||||||
|
|
||||||
* logrus/core: revert #208
|
|
||||||
|
|
||||||
# 0.8.4
|
|
||||||
|
|
||||||
* formatter/text: fix data race (#218)
|
|
||||||
|
|
||||||
# 0.8.3
|
|
||||||
|
|
||||||
* logrus/core: fix entry log level (#208)
|
|
||||||
* logrus/core: improve performance of text formatter by 40%
|
|
||||||
* logrus/core: expose `LevelHooks` type
|
|
||||||
* logrus/core: add support for DragonflyBSD and NetBSD
|
|
||||||
* formatter/text: print structs more verbosely
|
|
||||||
|
|
||||||
# 0.8.2
|
|
||||||
|
|
||||||
* logrus: fix more Fatal family functions
|
|
||||||
|
|
||||||
# 0.8.1
|
|
||||||
|
|
||||||
* logrus: fix not exiting on `Fatalf` and `Fatalln`
|
|
||||||
|
|
||||||
# 0.8.0
|
|
||||||
|
|
||||||
* logrus: defaults to stderr instead of stdout
|
|
||||||
* hooks/sentry: add special field for `*http.Request`
|
|
||||||
* formatter/text: ignore Windows for colors
|
|
||||||
|
|
||||||
# 0.7.3
|
|
||||||
|
|
||||||
* formatter/\*: allow configuration of timestamp layout
|
|
||||||
|
|
||||||
# 0.7.2
|
|
||||||
|
|
||||||
* formatter/text: Add configuration option for time format (#158)
|
|
||||||
21
vendor/github.com/sirupsen/logrus/LICENSE
generated
vendored
21
vendor/github.com/sirupsen/logrus/LICENSE
generated
vendored
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Simon Eskildsen
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
515
vendor/github.com/sirupsen/logrus/README.md
generated
vendored
515
vendor/github.com/sirupsen/logrus/README.md
generated
vendored
@@ -1,515 +0,0 @@
|
|||||||
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/> [](https://github.com/sirupsen/logrus/actions?query=workflow%3ACI) [](https://travis-ci.org/sirupsen/logrus) [](https://pkg.go.dev/github.com/sirupsen/logrus)
|
|
||||||
|
|
||||||
Logrus is a structured logger for Go (golang), completely API compatible with
|
|
||||||
the standard library logger.
|
|
||||||
|
|
||||||
**Logrus is in maintenance-mode.** We will not be introducing new features. It's
|
|
||||||
simply too hard to do in a way that won't break many people's projects, which is
|
|
||||||
the last thing you want from your Logging library (again...).
|
|
||||||
|
|
||||||
This does not mean Logrus is dead. Logrus will continue to be maintained for
|
|
||||||
security, (backwards compatible) bug fixes, and performance (where we are
|
|
||||||
limited by the interface).
|
|
||||||
|
|
||||||
I believe Logrus' biggest contribution is to have played a part in today's
|
|
||||||
widespread use of structured logging in Golang. There doesn't seem to be a
|
|
||||||
reason to do a major, breaking iteration into Logrus V2, since the fantastic Go
|
|
||||||
community has built those independently. Many fantastic alternatives have sprung
|
|
||||||
up. Logrus would look like those, had it been re-designed with what we know
|
|
||||||
about structured logging in Go today. Check out, for example,
|
|
||||||
[Zerolog][zerolog], [Zap][zap], and [Apex][apex].
|
|
||||||
|
|
||||||
[zerolog]: https://github.com/rs/zerolog
|
|
||||||
[zap]: https://github.com/uber-go/zap
|
|
||||||
[apex]: https://github.com/apex/log
|
|
||||||
|
|
||||||
**Seeing weird case-sensitive problems?** It's in the past been possible to
|
|
||||||
import Logrus as both upper- and lower-case. Due to the Go package environment,
|
|
||||||
this caused issues in the community and we needed a standard. Some environments
|
|
||||||
experienced problems with the upper-case variant, so the lower-case was decided.
|
|
||||||
Everything using `logrus` will need to use the lower-case:
|
|
||||||
`github.com/sirupsen/logrus`. Any package that isn't, should be changed.
|
|
||||||
|
|
||||||
To fix Glide, see [these
|
|
||||||
comments](https://github.com/sirupsen/logrus/issues/553#issuecomment-306591437).
|
|
||||||
For an in-depth explanation of the casing issue, see [this
|
|
||||||
comment](https://github.com/sirupsen/logrus/issues/570#issuecomment-313933276).
|
|
||||||
|
|
||||||
Nicely color-coded in development (when a TTY is attached, otherwise just
|
|
||||||
plain text):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
With `log.SetFormatter(&log.JSONFormatter{})`, for easy parsing by logstash
|
|
||||||
or Splunk:
|
|
||||||
|
|
||||||
```text
|
|
||||||
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
|
|
||||||
ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
|
|
||||||
|
|
||||||
{"level":"warning","msg":"The group's number increased tremendously!",
|
|
||||||
"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
|
|
||||||
|
|
||||||
{"animal":"walrus","level":"info","msg":"A giant walrus appears!",
|
|
||||||
"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
|
|
||||||
|
|
||||||
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
|
|
||||||
"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
|
|
||||||
|
|
||||||
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
|
|
||||||
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
|
|
||||||
```
|
|
||||||
|
|
||||||
With the default `log.SetFormatter(&log.TextFormatter{})` when a TTY is not
|
|
||||||
attached, the output is compatible with the
|
|
||||||
[logfmt](http://godoc.org/github.com/kr/logfmt) format:
|
|
||||||
|
|
||||||
```text
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
|
|
||||||
```
|
|
||||||
To ensure this behaviour even if a TTY is attached, set your formatter as follows:
|
|
||||||
|
|
||||||
```go
|
|
||||||
log.SetFormatter(&log.TextFormatter{
|
|
||||||
DisableColors: true,
|
|
||||||
FullTimestamp: true,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Logging Method Name
|
|
||||||
|
|
||||||
If you wish to add the calling method as a field, instruct the logger via:
|
|
||||||
```go
|
|
||||||
log.SetReportCaller(true)
|
|
||||||
```
|
|
||||||
This adds the caller as 'method' like so:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
|
|
||||||
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}
|
|
||||||
```
|
|
||||||
|
|
||||||
```text
|
|
||||||
time="2015-03-26T01:27:38-04:00" level=fatal method=github.com/sirupsen/arcticcreatures.migrate msg="a penguin swims by" animal=penguin
|
|
||||||
```
|
|
||||||
Note that this does add measurable overhead - the cost will depend on the version of Go, but is
|
|
||||||
between 20 and 40% in recent tests with 1.6 and 1.7. You can validate this in your
|
|
||||||
environment via benchmarks:
|
|
||||||
```
|
|
||||||
go test -bench=.*CallerTracing
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
#### Case-sensitivity
|
|
||||||
|
|
||||||
The organization's name was changed to lower-case--and this will not be changed
|
|
||||||
back. If you are getting import conflicts due to case sensitivity, please use
|
|
||||||
the lower-case import: `github.com/sirupsen/logrus`.
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
The simplest way to use Logrus is simply the package-level exported logger:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"animal": "walrus",
|
|
||||||
}).Info("A walrus appears")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that it's completely api-compatible with the stdlib logger, so you can
|
|
||||||
replace your `log` imports everywhere with `log "github.com/sirupsen/logrus"`
|
|
||||||
and you'll now have the flexibility of Logrus. You can customize it all you
|
|
||||||
want:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Log as JSON instead of the default ASCII formatter.
|
|
||||||
log.SetFormatter(&log.JSONFormatter{})
|
|
||||||
|
|
||||||
// Output to stdout instead of the default stderr
|
|
||||||
// Can be any io.Writer, see below for File example
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
|
|
||||||
// Only log the warning severity or above.
|
|
||||||
log.SetLevel(log.WarnLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"animal": "walrus",
|
|
||||||
"size": 10,
|
|
||||||
}).Info("A group of walrus emerges from the ocean")
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"omg": true,
|
|
||||||
"number": 122,
|
|
||||||
}).Warn("The group's number increased tremendously!")
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"omg": true,
|
|
||||||
"number": 100,
|
|
||||||
}).Fatal("The ice breaks!")
|
|
||||||
|
|
||||||
// A common pattern is to re-use fields between logging statements by re-using
|
|
||||||
// the logrus.Entry returned from WithFields()
|
|
||||||
contextLogger := log.WithFields(log.Fields{
|
|
||||||
"common": "this is a common field",
|
|
||||||
"other": "I also should be logged always",
|
|
||||||
})
|
|
||||||
|
|
||||||
contextLogger.Info("I'll be logged with common and other field")
|
|
||||||
contextLogger.Info("Me too")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For more advanced usage such as logging to multiple locations from the same
|
|
||||||
application, you can also create an instance of the `logrus` Logger:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create a new instance of the logger. You can have any number of instances.
|
|
||||||
var log = logrus.New()
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// The API for setting attributes is a little different than the package level
|
|
||||||
// exported logger. See Godoc.
|
|
||||||
log.Out = os.Stdout
|
|
||||||
|
|
||||||
// You could set this to any `io.Writer` such as a file
|
|
||||||
// file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
||||||
// if err == nil {
|
|
||||||
// log.Out = file
|
|
||||||
// } else {
|
|
||||||
// log.Info("Failed to log to file, using default stderr")
|
|
||||||
// }
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{
|
|
||||||
"animal": "walrus",
|
|
||||||
"size": 10,
|
|
||||||
}).Info("A group of walrus emerges from the ocean")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Fields
|
|
||||||
|
|
||||||
Logrus encourages careful, structured logging through logging fields instead of
|
|
||||||
long, unparseable error messages. For example, instead of: `log.Fatalf("Failed
|
|
||||||
to send event %s to topic %s with key %d")`, you should log the much more
|
|
||||||
discoverable:
|
|
||||||
|
|
||||||
```go
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"event": event,
|
|
||||||
"topic": topic,
|
|
||||||
"key": key,
|
|
||||||
}).Fatal("Failed to send event")
|
|
||||||
```
|
|
||||||
|
|
||||||
We've found this API forces you to think about logging in a way that produces
|
|
||||||
much more useful logging messages. We've been in countless situations where just
|
|
||||||
a single added field to a log statement that was already there would've saved us
|
|
||||||
hours. The `WithFields` call is optional.
|
|
||||||
|
|
||||||
In general, with Logrus using any of the `printf`-family functions should be
|
|
||||||
seen as a hint you should add a field, however, you can still use the
|
|
||||||
`printf`-family functions with Logrus.
|
|
||||||
|
|
||||||
#### Default Fields
|
|
||||||
|
|
||||||
Often it's helpful to have fields _always_ attached to log statements in an
|
|
||||||
application or parts of one. For example, you may want to always log the
|
|
||||||
`request_id` and `user_ip` in the context of a request. Instead of writing
|
|
||||||
`log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})` on
|
|
||||||
every line, you can create a `logrus.Entry` to pass around instead:
|
|
||||||
|
|
||||||
```go
|
|
||||||
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
|
|
||||||
requestLogger.Info("something happened on that request") # will log request_id and user_ip
|
|
||||||
requestLogger.Warn("something not great happened")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Hooks
|
|
||||||
|
|
||||||
You can add hooks for logging levels. For example to send errors to an exception
|
|
||||||
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
|
|
||||||
multiple places simultaneously, e.g. syslog.
|
|
||||||
|
|
||||||
Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
|
|
||||||
`init`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
|
|
||||||
logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
|
|
||||||
"log/syslog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
|
|
||||||
// Use the Airbrake hook to report errors that have Error severity or above to
|
|
||||||
// an exception tracker. You can create custom hooks, see the Hooks section.
|
|
||||||
log.AddHook(airbrake.NewHook(123, "xyz", "production"))
|
|
||||||
|
|
||||||
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to connect to local syslog daemon")
|
|
||||||
} else {
|
|
||||||
log.AddHook(hook)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Note: Syslog hook also support connecting to local syslog (Ex. "/dev/log" or "/var/run/syslog" or "/var/run/log"). For the detail, please check the [syslog hook README](hooks/syslog/README.md).
|
|
||||||
|
|
||||||
A list of currently known service hooks can be found in this wiki [page](https://github.com/sirupsen/logrus/wiki/Hooks)
|
|
||||||
|
|
||||||
|
|
||||||
#### Level logging
|
|
||||||
|
|
||||||
Logrus has seven logging levels: Trace, Debug, Info, Warning, Error, Fatal and Panic.
|
|
||||||
|
|
||||||
```go
|
|
||||||
log.Trace("Something very low level.")
|
|
||||||
log.Debug("Useful debugging information.")
|
|
||||||
log.Info("Something noteworthy happened!")
|
|
||||||
log.Warn("You should probably take a look at this.")
|
|
||||||
log.Error("Something failed but I'm not quitting.")
|
|
||||||
// Calls os.Exit(1) after logging
|
|
||||||
log.Fatal("Bye.")
|
|
||||||
// Calls panic() after logging
|
|
||||||
log.Panic("I'm bailing.")
|
|
||||||
```
|
|
||||||
|
|
||||||
You can set the logging level on a `Logger`, then it will only log entries with
|
|
||||||
that severity or anything above it:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// Will log anything that is info or above (warn, error, fatal, panic). Default.
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
```
|
|
||||||
|
|
||||||
It may be useful to set `log.Level = logrus.DebugLevel` in a debug or verbose
|
|
||||||
environment if your application has that.
|
|
||||||
|
|
||||||
Note: If you want different log levels for global (`log.SetLevel(...)`) and syslog logging, please check the [syslog hook README](hooks/syslog/README.md#different-log-levels-for-local-and-remote-logging).
|
|
||||||
|
|
||||||
#### Entries
|
|
||||||
|
|
||||||
Besides the fields added with `WithField` or `WithFields` some fields are
|
|
||||||
automatically added to all logging events:
|
|
||||||
|
|
||||||
1. `time`. The timestamp when the entry was created.
|
|
||||||
2. `msg`. The logging message passed to `{Info,Warn,Error,Fatal,Panic}` after
|
|
||||||
the `AddFields` call. E.g. `Failed to send event.`
|
|
||||||
3. `level`. The logging level. E.g. `info`.
|
|
||||||
|
|
||||||
#### Environments
|
|
||||||
|
|
||||||
Logrus has no notion of environment.
|
|
||||||
|
|
||||||
If you wish for hooks and formatters to only be used in specific environments,
|
|
||||||
you should handle that yourself. For example, if your application has a global
|
|
||||||
variable `Environment`, which is a string representation of the environment you
|
|
||||||
could do:
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// do something here to set environment depending on an environment variable
|
|
||||||
// or command-line flag
|
|
||||||
if Environment == "production" {
|
|
||||||
log.SetFormatter(&log.JSONFormatter{})
|
|
||||||
} else {
|
|
||||||
// The TextFormatter is default, you don't actually have to do this.
|
|
||||||
log.SetFormatter(&log.TextFormatter{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This configuration is how `logrus` was intended to be used, but JSON in
|
|
||||||
production is mostly only useful if you do log aggregation with tools like
|
|
||||||
Splunk or Logstash.
|
|
||||||
|
|
||||||
#### Formatters
|
|
||||||
|
|
||||||
The built-in logging formatters are:
|
|
||||||
|
|
||||||
* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise
|
|
||||||
without colors.
|
|
||||||
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
|
|
||||||
field to `true`. To force no colored output even if there is a TTY set the
|
|
||||||
`DisableColors` field to `true`. For Windows, see
|
|
||||||
[github.com/mattn/go-colorable](https://github.com/mattn/go-colorable).
|
|
||||||
* When colors are enabled, levels are truncated to 4 characters by default. To disable
|
|
||||||
truncation set the `DisableLevelTruncation` field to `true`.
|
|
||||||
* When outputting to a TTY, it's often helpful to visually scan down a column where all the levels are the same width. Setting the `PadLevelText` field to `true` enables this behavior, by adding padding to the level text.
|
|
||||||
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#TextFormatter).
|
|
||||||
* `logrus.JSONFormatter`. Logs fields as JSON.
|
|
||||||
* All options are listed in the [generated docs](https://godoc.org/github.com/sirupsen/logrus#JSONFormatter).
|
|
||||||
|
|
||||||
Third party logging formatters:
|
|
||||||
|
|
||||||
* [`FluentdFormatter`](https://github.com/joonix/log). Formats entries that can be parsed by Kubernetes and Google Container Engine.
|
|
||||||
* [`GELF`](https://github.com/fabienm/go-logrus-formatters). Formats entries so they comply to Graylog's [GELF 1.1 specification](http://docs.graylog.org/en/2.4/pages/gelf.html).
|
|
||||||
* [`logstash`](https://github.com/bshuster-repo/logrus-logstash-hook). Logs fields as [Logstash](http://logstash.net) Events.
|
|
||||||
* [`prefixed`](https://github.com/x-cray/logrus-prefixed-formatter). Displays log entry source along with alternative layout.
|
|
||||||
* [`zalgo`](https://github.com/aybabtme/logzalgo). Invoking the Power of Zalgo.
|
|
||||||
* [`nested-logrus-formatter`](https://github.com/antonfisher/nested-logrus-formatter). Converts logrus fields to a nested structure.
|
|
||||||
* [`powerful-logrus-formatter`](https://github.com/zput/zxcTool). get fileName, log's line number and the latest function's name when print log; Sava log to files.
|
|
||||||
* [`caption-json-formatter`](https://github.com/nolleh/caption_json_formatter). logrus's message json formatter with human-readable caption added.
|
|
||||||
|
|
||||||
You can define your formatter by implementing the `Formatter` interface,
|
|
||||||
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
|
|
||||||
`Fields` type (`map[string]interface{}`) with all your fields as well as the
|
|
||||||
default ones (see Entries section above):
|
|
||||||
|
|
||||||
```go
|
|
||||||
type MyJSONFormatter struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
log.SetFormatter(new(MyJSONFormatter))
|
|
||||||
|
|
||||||
func (f *MyJSONFormatter) Format(entry *Entry) ([]byte, error) {
|
|
||||||
// Note this doesn't include Time, Level and Message which are available on
|
|
||||||
// the Entry. Consult `godoc` on information about those fields or read the
|
|
||||||
// source of the official loggers.
|
|
||||||
serialized, err := json.Marshal(entry.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to marshal fields to JSON, %w", err)
|
|
||||||
}
|
|
||||||
return append(serialized, '\n'), nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Logger as an `io.Writer`
|
|
||||||
|
|
||||||
Logrus can be transformed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
|
|
||||||
|
|
||||||
```go
|
|
||||||
w := logger.Writer()
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
srv := http.Server{
|
|
||||||
// create a stdlib log.Logger that writes to
|
|
||||||
// logrus.Logger.
|
|
||||||
ErrorLog: log.New(w, "", 0),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each line written to that writer will be printed the usual way, using formatters
|
|
||||||
and hooks. The level for those entries is `info`.
|
|
||||||
|
|
||||||
This means that we can override the standard library logger easily:
|
|
||||||
|
|
||||||
```go
|
|
||||||
logger := logrus.New()
|
|
||||||
logger.Formatter = &logrus.JSONFormatter{}
|
|
||||||
|
|
||||||
// Use logrus for standard log output
|
|
||||||
// Note that `log` here references stdlib's log
|
|
||||||
// Not logrus imported under the name `log`.
|
|
||||||
log.SetOutput(logger.Writer())
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Rotation
|
|
||||||
|
|
||||||
Log rotation is not provided with Logrus. Log rotation should be done by an
|
|
||||||
external program (like `logrotate(8)`) that can compress and delete old log
|
|
||||||
entries. It should not be a feature of the application-level logger.
|
|
||||||
|
|
||||||
#### Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
| ---- | ----------- |
|
|
||||||
|[Logrus Mate](https://github.com/gogap/logrus_mate)|Logrus mate is a tool for Logrus to manage loggers, you can initial logger's level, hook and formatter by config file, the logger will be generated with different configs in different environments.|
|
|
||||||
|[Logrus Viper Helper](https://github.com/heirko/go-contrib/tree/master/logrusHelper)|An Helper around Logrus to wrap with spf13/Viper to load configuration with fangs! And to simplify Logrus configuration use some behavior of [Logrus Mate](https://github.com/gogap/logrus_mate). [sample](https://github.com/heirko/iris-contrib/blob/master/middleware/logrus-logger/example) |
|
|
||||||
|
|
||||||
#### Testing
|
|
||||||
|
|
||||||
Logrus has a built in facility for asserting the presence of log messages. This is implemented through the `test` hook and provides:
|
|
||||||
|
|
||||||
* decorators for existing logger (`test.NewLocal` and `test.NewGlobal`) which basically just adds the `test` hook
|
|
||||||
* a test logger (`test.NewNullLogger`) that just records log messages (and does not output any):
|
|
||||||
|
|
||||||
```go
|
|
||||||
import(
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSomething(t*testing.T){
|
|
||||||
logger, hook := test.NewNullLogger()
|
|
||||||
logger.Error("Helloerror")
|
|
||||||
|
|
||||||
assert.Equal(t, 1, len(hook.Entries))
|
|
||||||
assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
|
|
||||||
assert.Equal(t, "Helloerror", hook.LastEntry().Message)
|
|
||||||
|
|
||||||
hook.Reset()
|
|
||||||
assert.Nil(t, hook.LastEntry())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Fatal handlers
|
|
||||||
|
|
||||||
Logrus can register one or more functions that will be called when any `fatal`
|
|
||||||
level message is logged. The registered handlers will be executed before
|
|
||||||
logrus performs an `os.Exit(1)`. This behavior may be helpful if callers need
|
|
||||||
to gracefully shutdown. Unlike a `panic("Something went wrong...")` call which can be intercepted with a deferred `recover` a call to `os.Exit(1)` can not be intercepted.
|
|
||||||
|
|
||||||
```
|
|
||||||
...
|
|
||||||
handler := func() {
|
|
||||||
// gracefully shutdown something...
|
|
||||||
}
|
|
||||||
logrus.RegisterExitHandler(handler)
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Thread safety
|
|
||||||
|
|
||||||
By default, Logger is protected by a mutex for concurrent writes. The mutex is held when calling hooks and writing logs.
|
|
||||||
If you are sure such locking is not needed, you can call logger.SetNoLock() to disable the locking.
|
|
||||||
|
|
||||||
Situation when locking is not needed includes:
|
|
||||||
|
|
||||||
* You have no hooks registered, or hooks calling is already thread-safe.
|
|
||||||
|
|
||||||
* Writing to logger.Out is already thread-safe, for example:
|
|
||||||
|
|
||||||
1) logger.Out is protected by locks.
|
|
||||||
|
|
||||||
2) logger.Out is an os.File handler opened with `O_APPEND` flag, and every write is smaller than 4k. (This allows multi-thread/multi-process writing)
|
|
||||||
|
|
||||||
(Refer to http://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/)
|
|
||||||
76
vendor/github.com/sirupsen/logrus/alt_exit.go
generated
vendored
76
vendor/github.com/sirupsen/logrus/alt_exit.go
generated
vendored
@@ -1,76 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
// The following code was sourced and modified from the
|
|
||||||
// https://github.com/tebeka/atexit package governed by the following license:
|
|
||||||
//
|
|
||||||
// Copyright (c) 2012 Miki Tebeka <miki.tebeka@gmail.com>.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
// this software and associated documentation files (the "Software"), to deal in
|
|
||||||
// the Software without restriction, including without limitation the rights to
|
|
||||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
// subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var handlers = []func(){}
|
|
||||||
|
|
||||||
func runHandler(handler func()) {
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "Error: Logrus exit handler error:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
handler()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runHandlers() {
|
|
||||||
for _, handler := range handlers {
|
|
||||||
runHandler(handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit runs all the Logrus atexit handlers and then terminates the program using os.Exit(code)
|
|
||||||
func Exit(code int) {
|
|
||||||
runHandlers()
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
|
|
||||||
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
|
|
||||||
// any Fatal log entry is made.
|
|
||||||
//
|
|
||||||
// This method is useful when a caller wishes to use logrus to log a fatal
|
|
||||||
// message but also needs to gracefully shutdown. An example usecase could be
|
|
||||||
// closing database connections, or sending a alert that the application is
|
|
||||||
// closing.
|
|
||||||
func RegisterExitHandler(handler func()) {
|
|
||||||
handlers = append(handlers, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
|
|
||||||
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
|
|
||||||
// any Fatal log entry is made.
|
|
||||||
//
|
|
||||||
// This method is useful when a caller wishes to use logrus to log a fatal
|
|
||||||
// message but also needs to gracefully shutdown. An example usecase could be
|
|
||||||
// closing database connections, or sending a alert that the application is
|
|
||||||
// closing.
|
|
||||||
func DeferExitHandler(handler func()) {
|
|
||||||
handlers = append([]func(){handler}, handlers...)
|
|
||||||
}
|
|
||||||
14
vendor/github.com/sirupsen/logrus/appveyor.yml
generated
vendored
14
vendor/github.com/sirupsen/logrus/appveyor.yml
generated
vendored
@@ -1,14 +0,0 @@
|
|||||||
version: "{build}"
|
|
||||||
platform: x64
|
|
||||||
clone_folder: c:\gopath\src\github.com\sirupsen\logrus
|
|
||||||
environment:
|
|
||||||
GOPATH: c:\gopath
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
install:
|
|
||||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
|
||||||
- go version
|
|
||||||
build_script:
|
|
||||||
- go get -t
|
|
||||||
- go test
|
|
||||||
43
vendor/github.com/sirupsen/logrus/buffer_pool.go
generated
vendored
43
vendor/github.com/sirupsen/logrus/buffer_pool.go
generated
vendored
@@ -1,43 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
bufferPool BufferPool
|
|
||||||
)
|
|
||||||
|
|
||||||
type BufferPool interface {
|
|
||||||
Put(*bytes.Buffer)
|
|
||||||
Get() *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultPool struct {
|
|
||||||
pool *sync.Pool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *defaultPool) Put(buf *bytes.Buffer) {
|
|
||||||
p.pool.Put(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *defaultPool) Get() *bytes.Buffer {
|
|
||||||
return p.pool.Get().(*bytes.Buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBufferPool allows to replace the default logrus buffer pool
|
|
||||||
// to better meets the specific needs of an application.
|
|
||||||
func SetBufferPool(bp BufferPool) {
|
|
||||||
bufferPool = bp
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
SetBufferPool(&defaultPool{
|
|
||||||
pool: &sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return new(bytes.Buffer)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
26
vendor/github.com/sirupsen/logrus/doc.go
generated
vendored
26
vendor/github.com/sirupsen/logrus/doc.go
generated
vendored
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
|
|
||||||
|
|
||||||
|
|
||||||
The simplest way to use Logrus is simply the package-level exported logger:
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"animal": "walrus",
|
|
||||||
"number": 1,
|
|
||||||
"size": 10,
|
|
||||||
}).Info("A walrus appears")
|
|
||||||
}
|
|
||||||
|
|
||||||
Output:
|
|
||||||
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
|
|
||||||
|
|
||||||
For a full guide visit https://github.com/sirupsen/logrus
|
|
||||||
*/
|
|
||||||
package logrus
|
|
||||||
442
vendor/github.com/sirupsen/logrus/entry.go
generated
vendored
442
vendor/github.com/sirupsen/logrus/entry.go
generated
vendored
@@ -1,442 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
|
|
||||||
// qualified package name, cached at first use
|
|
||||||
logrusPackage string
|
|
||||||
|
|
||||||
// Positions in the call stack when tracing to report the calling method
|
|
||||||
minimumCallerDepth int
|
|
||||||
|
|
||||||
// Used for caller information initialisation
|
|
||||||
callerInitOnce sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maximumCallerDepth int = 25
|
|
||||||
knownLogrusFrames int = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// start at the bottom of the stack before the package-name cache is primed
|
|
||||||
minimumCallerDepth = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the key when adding errors using WithError.
|
|
||||||
var ErrorKey = "error"
|
|
||||||
|
|
||||||
// An entry is the final or intermediate Logrus logging entry. It contains all
|
|
||||||
// the fields passed with WithField{,s}. It's finally logged when Trace, Debug,
|
|
||||||
// Info, Warn, Error, Fatal or Panic is called on it. These objects can be
|
|
||||||
// reused and passed around as much as you wish to avoid field duplication.
|
|
||||||
type Entry struct {
|
|
||||||
Logger *Logger
|
|
||||||
|
|
||||||
// Contains all the fields set by the user.
|
|
||||||
Data Fields
|
|
||||||
|
|
||||||
// Time at which the log entry was created
|
|
||||||
Time time.Time
|
|
||||||
|
|
||||||
// Level the log entry was logged at: Trace, Debug, Info, Warn, Error, Fatal or Panic
|
|
||||||
// This field will be set on entry firing and the value will be equal to the one in Logger struct field.
|
|
||||||
Level Level
|
|
||||||
|
|
||||||
// Calling method, with package name
|
|
||||||
Caller *runtime.Frame
|
|
||||||
|
|
||||||
// Message passed to Trace, Debug, Info, Warn, Error, Fatal or Panic
|
|
||||||
Message string
|
|
||||||
|
|
||||||
// When formatter is called in entry.log(), a Buffer may be set to entry
|
|
||||||
Buffer *bytes.Buffer
|
|
||||||
|
|
||||||
// Contains the context set by the user. Useful for hook processing etc.
|
|
||||||
Context context.Context
|
|
||||||
|
|
||||||
// err may contain a field formatting error
|
|
||||||
err string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEntry(logger *Logger) *Entry {
|
|
||||||
return &Entry{
|
|
||||||
Logger: logger,
|
|
||||||
// Default is three fields, plus one optional. Give a little extra room.
|
|
||||||
Data: make(Fields, 6),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Dup() *Entry {
|
|
||||||
data := make(Fields, len(entry.Data))
|
|
||||||
for k, v := range entry.Data {
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, Context: entry.Context, err: entry.err}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the bytes representation of this entry from the formatter.
|
|
||||||
func (entry *Entry) Bytes() ([]byte, error) {
|
|
||||||
return entry.Logger.Formatter.Format(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the string representation from the reader and ultimately the
|
|
||||||
// formatter.
|
|
||||||
func (entry *Entry) String() (string, error) {
|
|
||||||
serialized, err := entry.Bytes()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
str := string(serialized)
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
|
|
||||||
func (entry *Entry) WithError(err error) *Entry {
|
|
||||||
return entry.WithField(ErrorKey, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a context to the Entry.
|
|
||||||
func (entry *Entry) WithContext(ctx context.Context) *Entry {
|
|
||||||
dataCopy := make(Fields, len(entry.Data))
|
|
||||||
for k, v := range entry.Data {
|
|
||||||
dataCopy[k] = v
|
|
||||||
}
|
|
||||||
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: entry.Time, err: entry.err, Context: ctx}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a single field to the Entry.
|
|
||||||
func (entry *Entry) WithField(key string, value interface{}) *Entry {
|
|
||||||
return entry.WithFields(Fields{key: value})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a map of fields to the Entry.
|
|
||||||
func (entry *Entry) WithFields(fields Fields) *Entry {
|
|
||||||
data := make(Fields, len(entry.Data)+len(fields))
|
|
||||||
for k, v := range entry.Data {
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
fieldErr := entry.err
|
|
||||||
for k, v := range fields {
|
|
||||||
isErrField := false
|
|
||||||
if t := reflect.TypeOf(v); t != nil {
|
|
||||||
switch {
|
|
||||||
case t.Kind() == reflect.Func, t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Func:
|
|
||||||
isErrField = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isErrField {
|
|
||||||
tmp := fmt.Sprintf("can not add field %q", k)
|
|
||||||
if fieldErr != "" {
|
|
||||||
fieldErr = entry.err + ", " + tmp
|
|
||||||
} else {
|
|
||||||
fieldErr = tmp
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &Entry{Logger: entry.Logger, Data: data, Time: entry.Time, err: fieldErr, Context: entry.Context}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overrides the time of the Entry.
|
|
||||||
func (entry *Entry) WithTime(t time.Time) *Entry {
|
|
||||||
dataCopy := make(Fields, len(entry.Data))
|
|
||||||
for k, v := range entry.Data {
|
|
||||||
dataCopy[k] = v
|
|
||||||
}
|
|
||||||
return &Entry{Logger: entry.Logger, Data: dataCopy, Time: t, err: entry.err, Context: entry.Context}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPackageName reduces a fully qualified function name to the package name
|
|
||||||
// There really ought to be to be a better way...
|
|
||||||
func getPackageName(f string) string {
|
|
||||||
for {
|
|
||||||
lastPeriod := strings.LastIndex(f, ".")
|
|
||||||
lastSlash := strings.LastIndex(f, "/")
|
|
||||||
if lastPeriod > lastSlash {
|
|
||||||
f = f[:lastPeriod]
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// getCaller retrieves the name of the first non-logrus calling function
|
|
||||||
func getCaller() *runtime.Frame {
|
|
||||||
// cache this package's fully-qualified name
|
|
||||||
callerInitOnce.Do(func() {
|
|
||||||
pcs := make([]uintptr, maximumCallerDepth)
|
|
||||||
_ = runtime.Callers(0, pcs)
|
|
||||||
|
|
||||||
// dynamic get the package name and the minimum caller depth
|
|
||||||
for i := 0; i < maximumCallerDepth; i++ {
|
|
||||||
funcName := runtime.FuncForPC(pcs[i]).Name()
|
|
||||||
if strings.Contains(funcName, "getCaller") {
|
|
||||||
logrusPackage = getPackageName(funcName)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
minimumCallerDepth = knownLogrusFrames
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restrict the lookback frames to avoid runaway lookups
|
|
||||||
pcs := make([]uintptr, maximumCallerDepth)
|
|
||||||
depth := runtime.Callers(minimumCallerDepth, pcs)
|
|
||||||
frames := runtime.CallersFrames(pcs[:depth])
|
|
||||||
|
|
||||||
for f, again := frames.Next(); again; f, again = frames.Next() {
|
|
||||||
pkg := getPackageName(f.Function)
|
|
||||||
|
|
||||||
// If the caller isn't part of this package, we're done
|
|
||||||
if pkg != logrusPackage {
|
|
||||||
return &f //nolint:scopelint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we got here, we failed to find the caller's context
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry Entry) HasCaller() (has bool) {
|
|
||||||
return entry.Logger != nil &&
|
|
||||||
entry.Logger.ReportCaller &&
|
|
||||||
entry.Caller != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) log(level Level, msg string) {
|
|
||||||
var buffer *bytes.Buffer
|
|
||||||
|
|
||||||
newEntry := entry.Dup()
|
|
||||||
|
|
||||||
if newEntry.Time.IsZero() {
|
|
||||||
newEntry.Time = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
newEntry.Level = level
|
|
||||||
newEntry.Message = msg
|
|
||||||
|
|
||||||
newEntry.Logger.mu.Lock()
|
|
||||||
reportCaller := newEntry.Logger.ReportCaller
|
|
||||||
bufPool := newEntry.getBufferPool()
|
|
||||||
newEntry.Logger.mu.Unlock()
|
|
||||||
|
|
||||||
if reportCaller {
|
|
||||||
newEntry.Caller = getCaller()
|
|
||||||
}
|
|
||||||
|
|
||||||
newEntry.fireHooks()
|
|
||||||
buffer = bufPool.Get()
|
|
||||||
defer func() {
|
|
||||||
newEntry.Buffer = nil
|
|
||||||
buffer.Reset()
|
|
||||||
bufPool.Put(buffer)
|
|
||||||
}()
|
|
||||||
buffer.Reset()
|
|
||||||
newEntry.Buffer = buffer
|
|
||||||
|
|
||||||
newEntry.write()
|
|
||||||
|
|
||||||
newEntry.Buffer = nil
|
|
||||||
|
|
||||||
// To avoid Entry#log() returning a value that only would make sense for
|
|
||||||
// panic() to use in Entry#Panic(), we avoid the allocation by checking
|
|
||||||
// directly here.
|
|
||||||
if level <= PanicLevel {
|
|
||||||
panic(newEntry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) getBufferPool() (pool BufferPool) {
|
|
||||||
if entry.Logger.BufferPool != nil {
|
|
||||||
return entry.Logger.BufferPool
|
|
||||||
}
|
|
||||||
return bufferPool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) fireHooks() {
|
|
||||||
var tmpHooks LevelHooks
|
|
||||||
entry.Logger.mu.Lock()
|
|
||||||
tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
|
|
||||||
for k, v := range entry.Logger.Hooks {
|
|
||||||
tmpHooks[k] = v
|
|
||||||
}
|
|
||||||
entry.Logger.mu.Unlock()
|
|
||||||
|
|
||||||
err := tmpHooks.Fire(entry.Level, entry)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) write() {
|
|
||||||
entry.Logger.mu.Lock()
|
|
||||||
defer entry.Logger.mu.Unlock()
|
|
||||||
serialized, err := entry.Logger.Formatter.Format(entry)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := entry.Logger.Out.Write(serialized); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log will log a message at the level given as parameter.
|
|
||||||
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
|
|
||||||
// For this behaviour Entry.Panic or Entry.Fatal should be used instead.
|
|
||||||
func (entry *Entry) Log(level Level, args ...interface{}) {
|
|
||||||
if entry.Logger.IsLevelEnabled(level) {
|
|
||||||
entry.log(level, fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Trace(args ...interface{}) {
|
|
||||||
entry.Log(TraceLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Debug(args ...interface{}) {
|
|
||||||
entry.Log(DebugLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Print(args ...interface{}) {
|
|
||||||
entry.Info(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Info(args ...interface{}) {
|
|
||||||
entry.Log(InfoLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Warn(args ...interface{}) {
|
|
||||||
entry.Log(WarnLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Warning(args ...interface{}) {
|
|
||||||
entry.Warn(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Error(args ...interface{}) {
|
|
||||||
entry.Log(ErrorLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Fatal(args ...interface{}) {
|
|
||||||
entry.Log(FatalLevel, args...)
|
|
||||||
entry.Logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Panic(args ...interface{}) {
|
|
||||||
entry.Log(PanicLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry Printf family functions
|
|
||||||
|
|
||||||
func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
|
|
||||||
if entry.Logger.IsLevelEnabled(level) {
|
|
||||||
entry.Log(level, fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Tracef(format string, args ...interface{}) {
|
|
||||||
entry.Logf(TraceLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Debugf(format string, args ...interface{}) {
|
|
||||||
entry.Logf(DebugLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Infof(format string, args ...interface{}) {
|
|
||||||
entry.Logf(InfoLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Printf(format string, args ...interface{}) {
|
|
||||||
entry.Infof(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Warnf(format string, args ...interface{}) {
|
|
||||||
entry.Logf(WarnLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Warningf(format string, args ...interface{}) {
|
|
||||||
entry.Warnf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Errorf(format string, args ...interface{}) {
|
|
||||||
entry.Logf(ErrorLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Fatalf(format string, args ...interface{}) {
|
|
||||||
entry.Logf(FatalLevel, format, args...)
|
|
||||||
entry.Logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Panicf(format string, args ...interface{}) {
|
|
||||||
entry.Logf(PanicLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry Println family functions
|
|
||||||
|
|
||||||
func (entry *Entry) Logln(level Level, args ...interface{}) {
|
|
||||||
if entry.Logger.IsLevelEnabled(level) {
|
|
||||||
entry.Log(level, entry.sprintlnn(args...))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Traceln(args ...interface{}) {
|
|
||||||
entry.Logln(TraceLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Debugln(args ...interface{}) {
|
|
||||||
entry.Logln(DebugLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Infoln(args ...interface{}) {
|
|
||||||
entry.Logln(InfoLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Println(args ...interface{}) {
|
|
||||||
entry.Infoln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Warnln(args ...interface{}) {
|
|
||||||
entry.Logln(WarnLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Warningln(args ...interface{}) {
|
|
||||||
entry.Warnln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Errorln(args ...interface{}) {
|
|
||||||
entry.Logln(ErrorLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Fatalln(args ...interface{}) {
|
|
||||||
entry.Logln(FatalLevel, args...)
|
|
||||||
entry.Logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (entry *Entry) Panicln(args ...interface{}) {
|
|
||||||
entry.Logln(PanicLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sprintlnn => Sprint no newline. This is to get the behavior of how
|
|
||||||
// fmt.Sprintln where spaces are always added between operands, regardless of
|
|
||||||
// their type. Instead of vendoring the Sprintln implementation to spare a
|
|
||||||
// string allocation, we do the simplest thing.
|
|
||||||
func (entry *Entry) sprintlnn(args ...interface{}) string {
|
|
||||||
msg := fmt.Sprintln(args...)
|
|
||||||
return msg[:len(msg)-1]
|
|
||||||
}
|
|
||||||
270
vendor/github.com/sirupsen/logrus/exported.go
generated
vendored
270
vendor/github.com/sirupsen/logrus/exported.go
generated
vendored
@@ -1,270 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// std is the name of the standard logger in stdlib `log`
|
|
||||||
std = New()
|
|
||||||
)
|
|
||||||
|
|
||||||
func StandardLogger() *Logger {
|
|
||||||
return std
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOutput sets the standard logger output.
|
|
||||||
func SetOutput(out io.Writer) {
|
|
||||||
std.SetOutput(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFormatter sets the standard logger formatter.
|
|
||||||
func SetFormatter(formatter Formatter) {
|
|
||||||
std.SetFormatter(formatter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetReportCaller sets whether the standard logger will include the calling
|
|
||||||
// method as a field.
|
|
||||||
func SetReportCaller(include bool) {
|
|
||||||
std.SetReportCaller(include)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLevel sets the standard logger level.
|
|
||||||
func SetLevel(level Level) {
|
|
||||||
std.SetLevel(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLevel returns the standard logger level.
|
|
||||||
func GetLevel() Level {
|
|
||||||
return std.GetLevel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
|
|
||||||
func IsLevelEnabled(level Level) bool {
|
|
||||||
return std.IsLevelEnabled(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddHook adds a hook to the standard logger hooks.
|
|
||||||
func AddHook(hook Hook) {
|
|
||||||
std.AddHook(hook)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
|
|
||||||
func WithError(err error) *Entry {
|
|
||||||
return std.WithField(ErrorKey, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithContext creates an entry from the standard logger and adds a context to it.
|
|
||||||
func WithContext(ctx context.Context) *Entry {
|
|
||||||
return std.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithField creates an entry from the standard logger and adds a field to
|
|
||||||
// it. If you want multiple fields, use `WithFields`.
|
|
||||||
//
|
|
||||||
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
|
||||||
// or Panic on the Entry it returns.
|
|
||||||
func WithField(key string, value interface{}) *Entry {
|
|
||||||
return std.WithField(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithFields creates an entry from the standard logger and adds multiple
|
|
||||||
// fields to it. This is simply a helper for `WithField`, invoking it
|
|
||||||
// once for each field.
|
|
||||||
//
|
|
||||||
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
|
||||||
// or Panic on the Entry it returns.
|
|
||||||
func WithFields(fields Fields) *Entry {
|
|
||||||
return std.WithFields(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTime creates an entry from the standard logger and overrides the time of
|
|
||||||
// logs generated with it.
|
|
||||||
//
|
|
||||||
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
|
|
||||||
// or Panic on the Entry it returns.
|
|
||||||
func WithTime(t time.Time) *Entry {
|
|
||||||
return std.WithTime(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trace logs a message at level Trace on the standard logger.
|
|
||||||
func Trace(args ...interface{}) {
|
|
||||||
std.Trace(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug logs a message at level Debug on the standard logger.
|
|
||||||
func Debug(args ...interface{}) {
|
|
||||||
std.Debug(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print logs a message at level Info on the standard logger.
|
|
||||||
func Print(args ...interface{}) {
|
|
||||||
std.Print(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info logs a message at level Info on the standard logger.
|
|
||||||
func Info(args ...interface{}) {
|
|
||||||
std.Info(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn logs a message at level Warn on the standard logger.
|
|
||||||
func Warn(args ...interface{}) {
|
|
||||||
std.Warn(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warning logs a message at level Warn on the standard logger.
|
|
||||||
func Warning(args ...interface{}) {
|
|
||||||
std.Warning(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error logs a message at level Error on the standard logger.
|
|
||||||
func Error(args ...interface{}) {
|
|
||||||
std.Error(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panic logs a message at level Panic on the standard logger.
|
|
||||||
func Panic(args ...interface{}) {
|
|
||||||
std.Panic(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
|
||||||
func Fatal(args ...interface{}) {
|
|
||||||
std.Fatal(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TraceFn logs a message from a func at level Trace on the standard logger.
|
|
||||||
func TraceFn(fn LogFunction) {
|
|
||||||
std.TraceFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DebugFn logs a message from a func at level Debug on the standard logger.
|
|
||||||
func DebugFn(fn LogFunction) {
|
|
||||||
std.DebugFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrintFn logs a message from a func at level Info on the standard logger.
|
|
||||||
func PrintFn(fn LogFunction) {
|
|
||||||
std.PrintFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InfoFn logs a message from a func at level Info on the standard logger.
|
|
||||||
func InfoFn(fn LogFunction) {
|
|
||||||
std.InfoFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarnFn logs a message from a func at level Warn on the standard logger.
|
|
||||||
func WarnFn(fn LogFunction) {
|
|
||||||
std.WarnFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WarningFn logs a message from a func at level Warn on the standard logger.
|
|
||||||
func WarningFn(fn LogFunction) {
|
|
||||||
std.WarningFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrorFn logs a message from a func at level Error on the standard logger.
|
|
||||||
func ErrorFn(fn LogFunction) {
|
|
||||||
std.ErrorFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PanicFn logs a message from a func at level Panic on the standard logger.
|
|
||||||
func PanicFn(fn LogFunction) {
|
|
||||||
std.PanicFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FatalFn logs a message from a func at level Fatal on the standard logger then the process will exit with status set to 1.
|
|
||||||
func FatalFn(fn LogFunction) {
|
|
||||||
std.FatalFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tracef logs a message at level Trace on the standard logger.
|
|
||||||
func Tracef(format string, args ...interface{}) {
|
|
||||||
std.Tracef(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugf logs a message at level Debug on the standard logger.
|
|
||||||
func Debugf(format string, args ...interface{}) {
|
|
||||||
std.Debugf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Printf logs a message at level Info on the standard logger.
|
|
||||||
func Printf(format string, args ...interface{}) {
|
|
||||||
std.Printf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infof logs a message at level Info on the standard logger.
|
|
||||||
func Infof(format string, args ...interface{}) {
|
|
||||||
std.Infof(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warnf logs a message at level Warn on the standard logger.
|
|
||||||
func Warnf(format string, args ...interface{}) {
|
|
||||||
std.Warnf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warningf logs a message at level Warn on the standard logger.
|
|
||||||
func Warningf(format string, args ...interface{}) {
|
|
||||||
std.Warningf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errorf logs a message at level Error on the standard logger.
|
|
||||||
func Errorf(format string, args ...interface{}) {
|
|
||||||
std.Errorf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panicf logs a message at level Panic on the standard logger.
|
|
||||||
func Panicf(format string, args ...interface{}) {
|
|
||||||
std.Panicf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
|
||||||
func Fatalf(format string, args ...interface{}) {
|
|
||||||
std.Fatalf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traceln logs a message at level Trace on the standard logger.
|
|
||||||
func Traceln(args ...interface{}) {
|
|
||||||
std.Traceln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugln logs a message at level Debug on the standard logger.
|
|
||||||
func Debugln(args ...interface{}) {
|
|
||||||
std.Debugln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Println logs a message at level Info on the standard logger.
|
|
||||||
func Println(args ...interface{}) {
|
|
||||||
std.Println(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infoln logs a message at level Info on the standard logger.
|
|
||||||
func Infoln(args ...interface{}) {
|
|
||||||
std.Infoln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warnln logs a message at level Warn on the standard logger.
|
|
||||||
func Warnln(args ...interface{}) {
|
|
||||||
std.Warnln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warningln logs a message at level Warn on the standard logger.
|
|
||||||
func Warningln(args ...interface{}) {
|
|
||||||
std.Warningln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Errorln logs a message at level Error on the standard logger.
|
|
||||||
func Errorln(args ...interface{}) {
|
|
||||||
std.Errorln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panicln logs a message at level Panic on the standard logger.
|
|
||||||
func Panicln(args ...interface{}) {
|
|
||||||
std.Panicln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
|
|
||||||
func Fatalln(args ...interface{}) {
|
|
||||||
std.Fatalln(args...)
|
|
||||||
}
|
|
||||||
78
vendor/github.com/sirupsen/logrus/formatter.go
generated
vendored
78
vendor/github.com/sirupsen/logrus/formatter.go
generated
vendored
@@ -1,78 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Default key names for the default fields
|
|
||||||
const (
|
|
||||||
defaultTimestampFormat = time.RFC3339
|
|
||||||
FieldKeyMsg = "msg"
|
|
||||||
FieldKeyLevel = "level"
|
|
||||||
FieldKeyTime = "time"
|
|
||||||
FieldKeyLogrusError = "logrus_error"
|
|
||||||
FieldKeyFunc = "func"
|
|
||||||
FieldKeyFile = "file"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The Formatter interface is used to implement a custom Formatter. It takes an
|
|
||||||
// `Entry`. It exposes all the fields, including the default ones:
|
|
||||||
//
|
|
||||||
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
|
|
||||||
// * `entry.Data["time"]`. The timestamp.
|
|
||||||
// * `entry.Data["level"]. The level the entry was logged at.
|
|
||||||
//
|
|
||||||
// Any additional fields added with `WithField` or `WithFields` are also in
|
|
||||||
// `entry.Data`. Format is expected to return an array of bytes which are then
|
|
||||||
// logged to `logger.Out`.
|
|
||||||
type Formatter interface {
|
|
||||||
Format(*Entry) ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
|
|
||||||
// dumping it. If this code wasn't there doing:
|
|
||||||
//
|
|
||||||
// logrus.WithField("level", 1).Info("hello")
|
|
||||||
//
|
|
||||||
// Would just silently drop the user provided level. Instead with this code
|
|
||||||
// it'll logged as:
|
|
||||||
//
|
|
||||||
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
|
|
||||||
//
|
|
||||||
// It's not exported because it's still using Data in an opinionated way. It's to
|
|
||||||
// avoid code duplication between the two default formatters.
|
|
||||||
func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
|
|
||||||
timeKey := fieldMap.resolve(FieldKeyTime)
|
|
||||||
if t, ok := data[timeKey]; ok {
|
|
||||||
data["fields."+timeKey] = t
|
|
||||||
delete(data, timeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgKey := fieldMap.resolve(FieldKeyMsg)
|
|
||||||
if m, ok := data[msgKey]; ok {
|
|
||||||
data["fields."+msgKey] = m
|
|
||||||
delete(data, msgKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
levelKey := fieldMap.resolve(FieldKeyLevel)
|
|
||||||
if l, ok := data[levelKey]; ok {
|
|
||||||
data["fields."+levelKey] = l
|
|
||||||
delete(data, levelKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
|
|
||||||
if l, ok := data[logrusErrKey]; ok {
|
|
||||||
data["fields."+logrusErrKey] = l
|
|
||||||
delete(data, logrusErrKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If reportCaller is not set, 'func' will not conflict.
|
|
||||||
if reportCaller {
|
|
||||||
funcKey := fieldMap.resolve(FieldKeyFunc)
|
|
||||||
if l, ok := data[funcKey]; ok {
|
|
||||||
data["fields."+funcKey] = l
|
|
||||||
}
|
|
||||||
fileKey := fieldMap.resolve(FieldKeyFile)
|
|
||||||
if l, ok := data[fileKey]; ok {
|
|
||||||
data["fields."+fileKey] = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
vendor/github.com/sirupsen/logrus/hooks.go
generated
vendored
34
vendor/github.com/sirupsen/logrus/hooks.go
generated
vendored
@@ -1,34 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
// A hook to be fired when logging on the logging levels returned from
|
|
||||||
// `Levels()` on your implementation of the interface. Note that this is not
|
|
||||||
// fired in a goroutine or a channel with workers, you should handle such
|
|
||||||
// functionality yourself if your call is non-blocking and you don't wish for
|
|
||||||
// the logging calls for levels returned from `Levels()` to block.
|
|
||||||
type Hook interface {
|
|
||||||
Levels() []Level
|
|
||||||
Fire(*Entry) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Internal type for storing the hooks on a logger instance.
|
|
||||||
type LevelHooks map[Level][]Hook
|
|
||||||
|
|
||||||
// Add a hook to an instance of logger. This is called with
|
|
||||||
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
|
|
||||||
func (hooks LevelHooks) Add(hook Hook) {
|
|
||||||
for _, level := range hook.Levels() {
|
|
||||||
hooks[level] = append(hooks[level], hook)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire all the hooks for the passed level. Used by `entry.log` to fire
|
|
||||||
// appropriate hooks for a log entry.
|
|
||||||
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
|
|
||||||
for _, hook := range hooks[level] {
|
|
||||||
if err := hook.Fire(entry); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
128
vendor/github.com/sirupsen/logrus/json_formatter.go
generated
vendored
128
vendor/github.com/sirupsen/logrus/json_formatter.go
generated
vendored
@@ -1,128 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fieldKey string
|
|
||||||
|
|
||||||
// FieldMap allows customization of the key names for default fields.
|
|
||||||
type FieldMap map[fieldKey]string
|
|
||||||
|
|
||||||
func (f FieldMap) resolve(key fieldKey) string {
|
|
||||||
if k, ok := f[key]; ok {
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONFormatter formats logs into parsable json
|
|
||||||
type JSONFormatter struct {
|
|
||||||
// TimestampFormat sets the format used for marshaling timestamps.
|
|
||||||
// The format to use is the same than for time.Format or time.Parse from the standard
|
|
||||||
// library.
|
|
||||||
// The standard Library already provides a set of predefined format.
|
|
||||||
TimestampFormat string
|
|
||||||
|
|
||||||
// DisableTimestamp allows disabling automatic timestamps in output
|
|
||||||
DisableTimestamp bool
|
|
||||||
|
|
||||||
// DisableHTMLEscape allows disabling html escaping in output
|
|
||||||
DisableHTMLEscape bool
|
|
||||||
|
|
||||||
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
|
|
||||||
DataKey string
|
|
||||||
|
|
||||||
// FieldMap allows users to customize the names of keys for default fields.
|
|
||||||
// As an example:
|
|
||||||
// formatter := &JSONFormatter{
|
|
||||||
// FieldMap: FieldMap{
|
|
||||||
// FieldKeyTime: "@timestamp",
|
|
||||||
// FieldKeyLevel: "@level",
|
|
||||||
// FieldKeyMsg: "@message",
|
|
||||||
// FieldKeyFunc: "@caller",
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
FieldMap FieldMap
|
|
||||||
|
|
||||||
// CallerPrettyfier can be set by the user to modify the content
|
|
||||||
// of the function and file keys in the json data when ReportCaller is
|
|
||||||
// activated. If any of the returned value is the empty string the
|
|
||||||
// corresponding key will be removed from json fields.
|
|
||||||
CallerPrettyfier func(*runtime.Frame) (function string, file string)
|
|
||||||
|
|
||||||
// PrettyPrint will indent all json logs
|
|
||||||
PrettyPrint bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format renders a single log entry
|
|
||||||
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
|
|
||||||
data := make(Fields, len(entry.Data)+4)
|
|
||||||
for k, v := range entry.Data {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case error:
|
|
||||||
// Otherwise errors are ignored by `encoding/json`
|
|
||||||
// https://github.com/sirupsen/logrus/issues/137
|
|
||||||
data[k] = v.Error()
|
|
||||||
default:
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.DataKey != "" {
|
|
||||||
newData := make(Fields, 4)
|
|
||||||
newData[f.DataKey] = data
|
|
||||||
data = newData
|
|
||||||
}
|
|
||||||
|
|
||||||
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
|
|
||||||
|
|
||||||
timestampFormat := f.TimestampFormat
|
|
||||||
if timestampFormat == "" {
|
|
||||||
timestampFormat = defaultTimestampFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.err != "" {
|
|
||||||
data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
|
|
||||||
}
|
|
||||||
if !f.DisableTimestamp {
|
|
||||||
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
|
|
||||||
}
|
|
||||||
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
|
|
||||||
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
|
|
||||||
if entry.HasCaller() {
|
|
||||||
funcVal := entry.Caller.Function
|
|
||||||
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
|
||||||
if f.CallerPrettyfier != nil {
|
|
||||||
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
|
|
||||||
}
|
|
||||||
if funcVal != "" {
|
|
||||||
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
|
|
||||||
}
|
|
||||||
if fileVal != "" {
|
|
||||||
data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var b *bytes.Buffer
|
|
||||||
if entry.Buffer != nil {
|
|
||||||
b = entry.Buffer
|
|
||||||
} else {
|
|
||||||
b = &bytes.Buffer{}
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder := json.NewEncoder(b)
|
|
||||||
encoder.SetEscapeHTML(!f.DisableHTMLEscape)
|
|
||||||
if f.PrettyPrint {
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
}
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
417
vendor/github.com/sirupsen/logrus/logger.go
generated
vendored
417
vendor/github.com/sirupsen/logrus/logger.go
generated
vendored
@@ -1,417 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LogFunction For big messages, it can be more efficient to pass a function
|
|
||||||
// and only call it if the log level is actually enables rather than
|
|
||||||
// generating the log message and then checking if the level is enabled
|
|
||||||
type LogFunction func() []interface{}
|
|
||||||
|
|
||||||
type Logger struct {
|
|
||||||
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
|
|
||||||
// file, or leave it default which is `os.Stderr`. You can also set this to
|
|
||||||
// something more adventurous, such as logging to Kafka.
|
|
||||||
Out io.Writer
|
|
||||||
// Hooks for the logger instance. These allow firing events based on logging
|
|
||||||
// levels and log entries. For example, to send errors to an error tracking
|
|
||||||
// service, log to StatsD or dump the core on fatal errors.
|
|
||||||
Hooks LevelHooks
|
|
||||||
// All log entries pass through the formatter before logged to Out. The
|
|
||||||
// included formatters are `TextFormatter` and `JSONFormatter` for which
|
|
||||||
// TextFormatter is the default. In development (when a TTY is attached) it
|
|
||||||
// logs with colors, but to a file it wouldn't. You can easily implement your
|
|
||||||
// own that implements the `Formatter` interface, see the `README` or included
|
|
||||||
// formatters for examples.
|
|
||||||
Formatter Formatter
|
|
||||||
|
|
||||||
// Flag for whether to log caller info (off by default)
|
|
||||||
ReportCaller bool
|
|
||||||
|
|
||||||
// The logging level the logger should log at. This is typically (and defaults
|
|
||||||
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
|
|
||||||
// logged.
|
|
||||||
Level Level
|
|
||||||
// Used to sync writing to the log. Locking is enabled by Default
|
|
||||||
mu MutexWrap
|
|
||||||
// Reusable empty entry
|
|
||||||
entryPool sync.Pool
|
|
||||||
// Function to exit the application, defaults to `os.Exit()`
|
|
||||||
ExitFunc exitFunc
|
|
||||||
// The buffer pool used to format the log. If it is nil, the default global
|
|
||||||
// buffer pool will be used.
|
|
||||||
BufferPool BufferPool
|
|
||||||
}
|
|
||||||
|
|
||||||
type exitFunc func(int)
|
|
||||||
|
|
||||||
type MutexWrap struct {
|
|
||||||
lock sync.Mutex
|
|
||||||
disabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *MutexWrap) Lock() {
|
|
||||||
if !mw.disabled {
|
|
||||||
mw.lock.Lock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *MutexWrap) Unlock() {
|
|
||||||
if !mw.disabled {
|
|
||||||
mw.lock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mw *MutexWrap) Disable() {
|
|
||||||
mw.disabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new logger. Configuration should be set by changing `Formatter`,
|
|
||||||
// `Out` and `Hooks` directly on the default logger instance. You can also just
|
|
||||||
// instantiate your own:
|
|
||||||
//
|
|
||||||
// var log = &logrus.Logger{
|
|
||||||
// Out: os.Stderr,
|
|
||||||
// Formatter: new(logrus.TextFormatter),
|
|
||||||
// Hooks: make(logrus.LevelHooks),
|
|
||||||
// Level: logrus.DebugLevel,
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// It's recommended to make this a global instance called `log`.
|
|
||||||
func New() *Logger {
|
|
||||||
return &Logger{
|
|
||||||
Out: os.Stderr,
|
|
||||||
Formatter: new(TextFormatter),
|
|
||||||
Hooks: make(LevelHooks),
|
|
||||||
Level: InfoLevel,
|
|
||||||
ExitFunc: os.Exit,
|
|
||||||
ReportCaller: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) newEntry() *Entry {
|
|
||||||
entry, ok := logger.entryPool.Get().(*Entry)
|
|
||||||
if ok {
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
return NewEntry(logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) releaseEntry(entry *Entry) {
|
|
||||||
entry.Data = map[string]interface{}{}
|
|
||||||
logger.entryPool.Put(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithField allocates a new entry and adds a field to it.
|
|
||||||
// Debug, Print, Info, Warn, Error, Fatal or Panic must be then applied to
|
|
||||||
// this new returned entry.
|
|
||||||
// If you want multiple fields, use `WithFields`.
|
|
||||||
func (logger *Logger) WithField(key string, value interface{}) *Entry {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
defer logger.releaseEntry(entry)
|
|
||||||
return entry.WithField(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds a struct of fields to the log entry. All it does is call `WithField` for
|
|
||||||
// each `Field`.
|
|
||||||
func (logger *Logger) WithFields(fields Fields) *Entry {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
defer logger.releaseEntry(entry)
|
|
||||||
return entry.WithFields(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add an error as single field to the log entry. All it does is call
|
|
||||||
// `WithError` for the given `error`.
|
|
||||||
func (logger *Logger) WithError(err error) *Entry {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
defer logger.releaseEntry(entry)
|
|
||||||
return entry.WithError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a context to the log entry.
|
|
||||||
func (logger *Logger) WithContext(ctx context.Context) *Entry {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
defer logger.releaseEntry(entry)
|
|
||||||
return entry.WithContext(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overrides the time of the log entry.
|
|
||||||
func (logger *Logger) WithTime(t time.Time) *Entry {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
defer logger.releaseEntry(entry)
|
|
||||||
return entry.WithTime(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
|
|
||||||
if logger.IsLevelEnabled(level) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Logf(level, format, args...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Tracef(format string, args ...interface{}) {
|
|
||||||
logger.Logf(TraceLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Debugf(format string, args ...interface{}) {
|
|
||||||
logger.Logf(DebugLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Infof(format string, args ...interface{}) {
|
|
||||||
logger.Logf(InfoLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Printf(format string, args ...interface{}) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Printf(format, args...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Warnf(format string, args ...interface{}) {
|
|
||||||
logger.Logf(WarnLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Warningf(format string, args ...interface{}) {
|
|
||||||
logger.Warnf(format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Errorf(format string, args ...interface{}) {
|
|
||||||
logger.Logf(ErrorLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Fatalf(format string, args ...interface{}) {
|
|
||||||
logger.Logf(FatalLevel, format, args...)
|
|
||||||
logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Panicf(format string, args ...interface{}) {
|
|
||||||
logger.Logf(PanicLevel, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log will log a message at the level given as parameter.
|
|
||||||
// Warning: using Log at Panic or Fatal level will not respectively Panic nor Exit.
|
|
||||||
// For this behaviour Logger.Panic or Logger.Fatal should be used instead.
|
|
||||||
func (logger *Logger) Log(level Level, args ...interface{}) {
|
|
||||||
if logger.IsLevelEnabled(level) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Log(level, args...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) LogFn(level Level, fn LogFunction) {
|
|
||||||
if logger.IsLevelEnabled(level) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Log(level, fn()...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Trace(args ...interface{}) {
|
|
||||||
logger.Log(TraceLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Debug(args ...interface{}) {
|
|
||||||
logger.Log(DebugLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Info(args ...interface{}) {
|
|
||||||
logger.Log(InfoLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Print(args ...interface{}) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Print(args...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Warn(args ...interface{}) {
|
|
||||||
logger.Log(WarnLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Warning(args ...interface{}) {
|
|
||||||
logger.Warn(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Error(args ...interface{}) {
|
|
||||||
logger.Log(ErrorLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Fatal(args ...interface{}) {
|
|
||||||
logger.Log(FatalLevel, args...)
|
|
||||||
logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Panic(args ...interface{}) {
|
|
||||||
logger.Log(PanicLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) TraceFn(fn LogFunction) {
|
|
||||||
logger.LogFn(TraceLevel, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) DebugFn(fn LogFunction) {
|
|
||||||
logger.LogFn(DebugLevel, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) InfoFn(fn LogFunction) {
|
|
||||||
logger.LogFn(InfoLevel, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) PrintFn(fn LogFunction) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Print(fn()...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) WarnFn(fn LogFunction) {
|
|
||||||
logger.LogFn(WarnLevel, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) WarningFn(fn LogFunction) {
|
|
||||||
logger.WarnFn(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) ErrorFn(fn LogFunction) {
|
|
||||||
logger.LogFn(ErrorLevel, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) FatalFn(fn LogFunction) {
|
|
||||||
logger.LogFn(FatalLevel, fn)
|
|
||||||
logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) PanicFn(fn LogFunction) {
|
|
||||||
logger.LogFn(PanicLevel, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Logln(level Level, args ...interface{}) {
|
|
||||||
if logger.IsLevelEnabled(level) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Logln(level, args...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Traceln(args ...interface{}) {
|
|
||||||
logger.Logln(TraceLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Debugln(args ...interface{}) {
|
|
||||||
logger.Logln(DebugLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Infoln(args ...interface{}) {
|
|
||||||
logger.Logln(InfoLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Println(args ...interface{}) {
|
|
||||||
entry := logger.newEntry()
|
|
||||||
entry.Println(args...)
|
|
||||||
logger.releaseEntry(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Warnln(args ...interface{}) {
|
|
||||||
logger.Logln(WarnLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Warningln(args ...interface{}) {
|
|
||||||
logger.Warnln(args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Errorln(args ...interface{}) {
|
|
||||||
logger.Logln(ErrorLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Fatalln(args ...interface{}) {
|
|
||||||
logger.Logln(FatalLevel, args...)
|
|
||||||
logger.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Panicln(args ...interface{}) {
|
|
||||||
logger.Logln(PanicLevel, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) Exit(code int) {
|
|
||||||
runHandlers()
|
|
||||||
if logger.ExitFunc == nil {
|
|
||||||
logger.ExitFunc = os.Exit
|
|
||||||
}
|
|
||||||
logger.ExitFunc(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
//When file is opened with appending mode, it's safe to
|
|
||||||
//write concurrently to a file (within 4k message on Linux).
|
|
||||||
//In these cases user can choose to disable the lock.
|
|
||||||
func (logger *Logger) SetNoLock() {
|
|
||||||
logger.mu.Disable()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) level() Level {
|
|
||||||
return Level(atomic.LoadUint32((*uint32)(&logger.Level)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetLevel sets the logger level.
|
|
||||||
func (logger *Logger) SetLevel(level Level) {
|
|
||||||
atomic.StoreUint32((*uint32)(&logger.Level), uint32(level))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLevel returns the logger level.
|
|
||||||
func (logger *Logger) GetLevel() Level {
|
|
||||||
return logger.level()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddHook adds a hook to the logger hooks.
|
|
||||||
func (logger *Logger) AddHook(hook Hook) {
|
|
||||||
logger.mu.Lock()
|
|
||||||
defer logger.mu.Unlock()
|
|
||||||
logger.Hooks.Add(hook)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLevelEnabled checks if the log level of the logger is greater than the level param
|
|
||||||
func (logger *Logger) IsLevelEnabled(level Level) bool {
|
|
||||||
return logger.level() >= level
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFormatter sets the logger formatter.
|
|
||||||
func (logger *Logger) SetFormatter(formatter Formatter) {
|
|
||||||
logger.mu.Lock()
|
|
||||||
defer logger.mu.Unlock()
|
|
||||||
logger.Formatter = formatter
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOutput sets the logger output.
|
|
||||||
func (logger *Logger) SetOutput(output io.Writer) {
|
|
||||||
logger.mu.Lock()
|
|
||||||
defer logger.mu.Unlock()
|
|
||||||
logger.Out = output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (logger *Logger) SetReportCaller(reportCaller bool) {
|
|
||||||
logger.mu.Lock()
|
|
||||||
defer logger.mu.Unlock()
|
|
||||||
logger.ReportCaller = reportCaller
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceHooks replaces the logger hooks and returns the old ones
|
|
||||||
func (logger *Logger) ReplaceHooks(hooks LevelHooks) LevelHooks {
|
|
||||||
logger.mu.Lock()
|
|
||||||
oldHooks := logger.Hooks
|
|
||||||
logger.Hooks = hooks
|
|
||||||
logger.mu.Unlock()
|
|
||||||
return oldHooks
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetBufferPool sets the logger buffer pool.
|
|
||||||
func (logger *Logger) SetBufferPool(pool BufferPool) {
|
|
||||||
logger.mu.Lock()
|
|
||||||
defer logger.mu.Unlock()
|
|
||||||
logger.BufferPool = pool
|
|
||||||
}
|
|
||||||
186
vendor/github.com/sirupsen/logrus/logrus.go
generated
vendored
186
vendor/github.com/sirupsen/logrus/logrus.go
generated
vendored
@@ -1,186 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fields type, used to pass to `WithFields`.
|
|
||||||
type Fields map[string]interface{}
|
|
||||||
|
|
||||||
// Level type
|
|
||||||
type Level uint32
|
|
||||||
|
|
||||||
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
|
|
||||||
func (level Level) String() string {
|
|
||||||
if b, err := level.MarshalText(); err == nil {
|
|
||||||
return string(b)
|
|
||||||
} else {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseLevel takes a string level and returns the Logrus log level constant.
|
|
||||||
func ParseLevel(lvl string) (Level, error) {
|
|
||||||
switch strings.ToLower(lvl) {
|
|
||||||
case "panic":
|
|
||||||
return PanicLevel, nil
|
|
||||||
case "fatal":
|
|
||||||
return FatalLevel, nil
|
|
||||||
case "error":
|
|
||||||
return ErrorLevel, nil
|
|
||||||
case "warn", "warning":
|
|
||||||
return WarnLevel, nil
|
|
||||||
case "info":
|
|
||||||
return InfoLevel, nil
|
|
||||||
case "debug":
|
|
||||||
return DebugLevel, nil
|
|
||||||
case "trace":
|
|
||||||
return TraceLevel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var l Level
|
|
||||||
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
|
||||||
func (level *Level) UnmarshalText(text []byte) error {
|
|
||||||
l, err := ParseLevel(string(text))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*level = l
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (level Level) MarshalText() ([]byte, error) {
|
|
||||||
switch level {
|
|
||||||
case TraceLevel:
|
|
||||||
return []byte("trace"), nil
|
|
||||||
case DebugLevel:
|
|
||||||
return []byte("debug"), nil
|
|
||||||
case InfoLevel:
|
|
||||||
return []byte("info"), nil
|
|
||||||
case WarnLevel:
|
|
||||||
return []byte("warning"), nil
|
|
||||||
case ErrorLevel:
|
|
||||||
return []byte("error"), nil
|
|
||||||
case FatalLevel:
|
|
||||||
return []byte("fatal"), nil
|
|
||||||
case PanicLevel:
|
|
||||||
return []byte("panic"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("not a valid logrus level %d", level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A constant exposing all logging levels
|
|
||||||
var AllLevels = []Level{
|
|
||||||
PanicLevel,
|
|
||||||
FatalLevel,
|
|
||||||
ErrorLevel,
|
|
||||||
WarnLevel,
|
|
||||||
InfoLevel,
|
|
||||||
DebugLevel,
|
|
||||||
TraceLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
// These are the different logging levels. You can set the logging level to log
|
|
||||||
// on your instance of logger, obtained with `logrus.New()`.
|
|
||||||
const (
|
|
||||||
// PanicLevel level, highest level of severity. Logs and then calls panic with the
|
|
||||||
// message passed to Debug, Info, ...
|
|
||||||
PanicLevel Level = iota
|
|
||||||
// FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
|
|
||||||
// logging level is set to Panic.
|
|
||||||
FatalLevel
|
|
||||||
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
|
|
||||||
// Commonly used for hooks to send errors to an error tracking service.
|
|
||||||
ErrorLevel
|
|
||||||
// WarnLevel level. Non-critical entries that deserve eyes.
|
|
||||||
WarnLevel
|
|
||||||
// InfoLevel level. General operational entries about what's going on inside the
|
|
||||||
// application.
|
|
||||||
InfoLevel
|
|
||||||
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
|
|
||||||
DebugLevel
|
|
||||||
// TraceLevel level. Designates finer-grained informational events than the Debug.
|
|
||||||
TraceLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
// Won't compile if StdLogger can't be realized by a log.Logger
|
|
||||||
var (
|
|
||||||
_ StdLogger = &log.Logger{}
|
|
||||||
_ StdLogger = &Entry{}
|
|
||||||
_ StdLogger = &Logger{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// StdLogger is what your logrus-enabled library should take, that way
|
|
||||||
// it'll accept a stdlib logger and a logrus logger. There's no standard
|
|
||||||
// interface, this is the closest we get, unfortunately.
|
|
||||||
type StdLogger interface {
|
|
||||||
Print(...interface{})
|
|
||||||
Printf(string, ...interface{})
|
|
||||||
Println(...interface{})
|
|
||||||
|
|
||||||
Fatal(...interface{})
|
|
||||||
Fatalf(string, ...interface{})
|
|
||||||
Fatalln(...interface{})
|
|
||||||
|
|
||||||
Panic(...interface{})
|
|
||||||
Panicf(string, ...interface{})
|
|
||||||
Panicln(...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// The FieldLogger interface generalizes the Entry and Logger types
|
|
||||||
type FieldLogger interface {
|
|
||||||
WithField(key string, value interface{}) *Entry
|
|
||||||
WithFields(fields Fields) *Entry
|
|
||||||
WithError(err error) *Entry
|
|
||||||
|
|
||||||
Debugf(format string, args ...interface{})
|
|
||||||
Infof(format string, args ...interface{})
|
|
||||||
Printf(format string, args ...interface{})
|
|
||||||
Warnf(format string, args ...interface{})
|
|
||||||
Warningf(format string, args ...interface{})
|
|
||||||
Errorf(format string, args ...interface{})
|
|
||||||
Fatalf(format string, args ...interface{})
|
|
||||||
Panicf(format string, args ...interface{})
|
|
||||||
|
|
||||||
Debug(args ...interface{})
|
|
||||||
Info(args ...interface{})
|
|
||||||
Print(args ...interface{})
|
|
||||||
Warn(args ...interface{})
|
|
||||||
Warning(args ...interface{})
|
|
||||||
Error(args ...interface{})
|
|
||||||
Fatal(args ...interface{})
|
|
||||||
Panic(args ...interface{})
|
|
||||||
|
|
||||||
Debugln(args ...interface{})
|
|
||||||
Infoln(args ...interface{})
|
|
||||||
Println(args ...interface{})
|
|
||||||
Warnln(args ...interface{})
|
|
||||||
Warningln(args ...interface{})
|
|
||||||
Errorln(args ...interface{})
|
|
||||||
Fatalln(args ...interface{})
|
|
||||||
Panicln(args ...interface{})
|
|
||||||
|
|
||||||
// IsDebugEnabled() bool
|
|
||||||
// IsInfoEnabled() bool
|
|
||||||
// IsWarnEnabled() bool
|
|
||||||
// IsErrorEnabled() bool
|
|
||||||
// IsFatalEnabled() bool
|
|
||||||
// IsPanicEnabled() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is
|
|
||||||
// here for consistancy. Do not use. Use Logger or Entry instead.
|
|
||||||
type Ext1FieldLogger interface {
|
|
||||||
FieldLogger
|
|
||||||
Tracef(format string, args ...interface{})
|
|
||||||
Trace(args ...interface{})
|
|
||||||
Traceln(args ...interface{})
|
|
||||||
}
|
|
||||||
11
vendor/github.com/sirupsen/logrus/terminal_check_appengine.go
generated
vendored
11
vendor/github.com/sirupsen/logrus/terminal_check_appengine.go
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
// +build appengine
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkIfTerminal(w io.Writer) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
13
vendor/github.com/sirupsen/logrus/terminal_check_bsd.go
generated
vendored
13
vendor/github.com/sirupsen/logrus/terminal_check_bsd.go
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
// +build darwin dragonfly freebsd netbsd openbsd
|
|
||||||
// +build !js
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
const ioctlReadTermios = unix.TIOCGETA
|
|
||||||
|
|
||||||
func isTerminal(fd int) bool {
|
|
||||||
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
7
vendor/github.com/sirupsen/logrus/terminal_check_js.go
generated
vendored
7
vendor/github.com/sirupsen/logrus/terminal_check_js.go
generated
vendored
@@ -1,7 +0,0 @@
|
|||||||
// +build js
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
func isTerminal(fd int) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
11
vendor/github.com/sirupsen/logrus/terminal_check_no_terminal.go
generated
vendored
11
vendor/github.com/sirupsen/logrus/terminal_check_no_terminal.go
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
// +build js nacl plan9
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkIfTerminal(w io.Writer) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
17
vendor/github.com/sirupsen/logrus/terminal_check_notappengine.go
generated
vendored
17
vendor/github.com/sirupsen/logrus/terminal_check_notappengine.go
generated
vendored
@@ -1,17 +0,0 @@
|
|||||||
// +build !appengine,!js,!windows,!nacl,!plan9
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkIfTerminal(w io.Writer) bool {
|
|
||||||
switch v := w.(type) {
|
|
||||||
case *os.File:
|
|
||||||
return isTerminal(int(v.Fd()))
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
11
vendor/github.com/sirupsen/logrus/terminal_check_solaris.go
generated
vendored
11
vendor/github.com/sirupsen/logrus/terminal_check_solaris.go
generated
vendored
@@ -1,11 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
|
||||||
func isTerminal(fd int) bool {
|
|
||||||
_, err := unix.IoctlGetTermio(fd, unix.TCGETA)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
13
vendor/github.com/sirupsen/logrus/terminal_check_unix.go
generated
vendored
13
vendor/github.com/sirupsen/logrus/terminal_check_unix.go
generated
vendored
@@ -1,13 +0,0 @@
|
|||||||
// +build linux aix zos
|
|
||||||
// +build !js
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import "golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
const ioctlReadTermios = unix.TCGETS
|
|
||||||
|
|
||||||
func isTerminal(fd int) bool {
|
|
||||||
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
27
vendor/github.com/sirupsen/logrus/terminal_check_windows.go
generated
vendored
27
vendor/github.com/sirupsen/logrus/terminal_check_windows.go
generated
vendored
@@ -1,27 +0,0 @@
|
|||||||
// +build !appengine,!js,windows
|
|
||||||
|
|
||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkIfTerminal(w io.Writer) bool {
|
|
||||||
switch v := w.(type) {
|
|
||||||
case *os.File:
|
|
||||||
handle := windows.Handle(v.Fd())
|
|
||||||
var mode uint32
|
|
||||||
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
||||||
if err := windows.SetConsoleMode(handle, mode); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
339
vendor/github.com/sirupsen/logrus/text_formatter.go
generated
vendored
339
vendor/github.com/sirupsen/logrus/text_formatter.go
generated
vendored
@@ -1,339 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
red = 31
|
|
||||||
yellow = 33
|
|
||||||
blue = 36
|
|
||||||
gray = 37
|
|
||||||
)
|
|
||||||
|
|
||||||
var baseTimestamp time.Time
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
baseTimestamp = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextFormatter formats logs into text
|
|
||||||
type TextFormatter struct {
|
|
||||||
// Set to true to bypass checking for a TTY before outputting colors.
|
|
||||||
ForceColors bool
|
|
||||||
|
|
||||||
// Force disabling colors.
|
|
||||||
DisableColors bool
|
|
||||||
|
|
||||||
// Force quoting of all values
|
|
||||||
ForceQuote bool
|
|
||||||
|
|
||||||
// DisableQuote disables quoting for all values.
|
|
||||||
// DisableQuote will have a lower priority than ForceQuote.
|
|
||||||
// If both of them are set to true, quote will be forced on all values.
|
|
||||||
DisableQuote bool
|
|
||||||
|
|
||||||
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
|
|
||||||
EnvironmentOverrideColors bool
|
|
||||||
|
|
||||||
// Disable timestamp logging. useful when output is redirected to logging
|
|
||||||
// system that already adds timestamps.
|
|
||||||
DisableTimestamp bool
|
|
||||||
|
|
||||||
// Enable logging the full timestamp when a TTY is attached instead of just
|
|
||||||
// the time passed since beginning of execution.
|
|
||||||
FullTimestamp bool
|
|
||||||
|
|
||||||
// TimestampFormat to use for display when a full timestamp is printed.
|
|
||||||
// The format to use is the same than for time.Format or time.Parse from the standard
|
|
||||||
// library.
|
|
||||||
// The standard Library already provides a set of predefined format.
|
|
||||||
TimestampFormat string
|
|
||||||
|
|
||||||
// The fields are sorted by default for a consistent output. For applications
|
|
||||||
// that log extremely frequently and don't use the JSON formatter this may not
|
|
||||||
// be desired.
|
|
||||||
DisableSorting bool
|
|
||||||
|
|
||||||
// The keys sorting function, when uninitialized it uses sort.Strings.
|
|
||||||
SortingFunc func([]string)
|
|
||||||
|
|
||||||
// Disables the truncation of the level text to 4 characters.
|
|
||||||
DisableLevelTruncation bool
|
|
||||||
|
|
||||||
// PadLevelText Adds padding the level text so that all the levels output at the same length
|
|
||||||
// PadLevelText is a superset of the DisableLevelTruncation option
|
|
||||||
PadLevelText bool
|
|
||||||
|
|
||||||
// QuoteEmptyFields will wrap empty fields in quotes if true
|
|
||||||
QuoteEmptyFields bool
|
|
||||||
|
|
||||||
// Whether the logger's out is to a terminal
|
|
||||||
isTerminal bool
|
|
||||||
|
|
||||||
// FieldMap allows users to customize the names of keys for default fields.
|
|
||||||
// As an example:
|
|
||||||
// formatter := &TextFormatter{
|
|
||||||
// FieldMap: FieldMap{
|
|
||||||
// FieldKeyTime: "@timestamp",
|
|
||||||
// FieldKeyLevel: "@level",
|
|
||||||
// FieldKeyMsg: "@message"}}
|
|
||||||
FieldMap FieldMap
|
|
||||||
|
|
||||||
// CallerPrettyfier can be set by the user to modify the content
|
|
||||||
// of the function and file keys in the data when ReportCaller is
|
|
||||||
// activated. If any of the returned value is the empty string the
|
|
||||||
// corresponding key will be removed from fields.
|
|
||||||
CallerPrettyfier func(*runtime.Frame) (function string, file string)
|
|
||||||
|
|
||||||
terminalInitOnce sync.Once
|
|
||||||
|
|
||||||
// The max length of the level text, generated dynamically on init
|
|
||||||
levelTextMaxLength int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TextFormatter) init(entry *Entry) {
|
|
||||||
if entry.Logger != nil {
|
|
||||||
f.isTerminal = checkIfTerminal(entry.Logger.Out)
|
|
||||||
}
|
|
||||||
// Get the max length of the level text
|
|
||||||
for _, level := range AllLevels {
|
|
||||||
levelTextLength := utf8.RuneCount([]byte(level.String()))
|
|
||||||
if levelTextLength > f.levelTextMaxLength {
|
|
||||||
f.levelTextMaxLength = levelTextLength
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TextFormatter) isColored() bool {
|
|
||||||
isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
|
|
||||||
|
|
||||||
if f.EnvironmentOverrideColors {
|
|
||||||
switch force, ok := os.LookupEnv("CLICOLOR_FORCE"); {
|
|
||||||
case ok && force != "0":
|
|
||||||
isColored = true
|
|
||||||
case ok && force == "0", os.Getenv("CLICOLOR") == "0":
|
|
||||||
isColored = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isColored && !f.DisableColors
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format renders a single log entry
|
|
||||||
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
|
|
||||||
data := make(Fields)
|
|
||||||
for k, v := range entry.Data {
|
|
||||||
data[k] = v
|
|
||||||
}
|
|
||||||
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
|
|
||||||
keys := make([]string, 0, len(data))
|
|
||||||
for k := range data {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
var funcVal, fileVal string
|
|
||||||
|
|
||||||
fixedKeys := make([]string, 0, 4+len(data))
|
|
||||||
if !f.DisableTimestamp {
|
|
||||||
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
|
|
||||||
}
|
|
||||||
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
|
|
||||||
if entry.Message != "" {
|
|
||||||
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
|
|
||||||
}
|
|
||||||
if entry.err != "" {
|
|
||||||
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
|
|
||||||
}
|
|
||||||
if entry.HasCaller() {
|
|
||||||
if f.CallerPrettyfier != nil {
|
|
||||||
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
|
|
||||||
} else {
|
|
||||||
funcVal = entry.Caller.Function
|
|
||||||
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if funcVal != "" {
|
|
||||||
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
|
|
||||||
}
|
|
||||||
if fileVal != "" {
|
|
||||||
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.DisableSorting {
|
|
||||||
if f.SortingFunc == nil {
|
|
||||||
sort.Strings(keys)
|
|
||||||
fixedKeys = append(fixedKeys, keys...)
|
|
||||||
} else {
|
|
||||||
if !f.isColored() {
|
|
||||||
fixedKeys = append(fixedKeys, keys...)
|
|
||||||
f.SortingFunc(fixedKeys)
|
|
||||||
} else {
|
|
||||||
f.SortingFunc(keys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fixedKeys = append(fixedKeys, keys...)
|
|
||||||
}
|
|
||||||
|
|
||||||
var b *bytes.Buffer
|
|
||||||
if entry.Buffer != nil {
|
|
||||||
b = entry.Buffer
|
|
||||||
} else {
|
|
||||||
b = &bytes.Buffer{}
|
|
||||||
}
|
|
||||||
|
|
||||||
f.terminalInitOnce.Do(func() { f.init(entry) })
|
|
||||||
|
|
||||||
timestampFormat := f.TimestampFormat
|
|
||||||
if timestampFormat == "" {
|
|
||||||
timestampFormat = defaultTimestampFormat
|
|
||||||
}
|
|
||||||
if f.isColored() {
|
|
||||||
f.printColored(b, entry, keys, data, timestampFormat)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
for _, key := range fixedKeys {
|
|
||||||
var value interface{}
|
|
||||||
switch {
|
|
||||||
case key == f.FieldMap.resolve(FieldKeyTime):
|
|
||||||
value = entry.Time.Format(timestampFormat)
|
|
||||||
case key == f.FieldMap.resolve(FieldKeyLevel):
|
|
||||||
value = entry.Level.String()
|
|
||||||
case key == f.FieldMap.resolve(FieldKeyMsg):
|
|
||||||
value = entry.Message
|
|
||||||
case key == f.FieldMap.resolve(FieldKeyLogrusError):
|
|
||||||
value = entry.err
|
|
||||||
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
|
|
||||||
value = funcVal
|
|
||||||
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
|
|
||||||
value = fileVal
|
|
||||||
default:
|
|
||||||
value = data[key]
|
|
||||||
}
|
|
||||||
f.appendKeyValue(b, key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteByte('\n')
|
|
||||||
return b.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
|
|
||||||
var levelColor int
|
|
||||||
switch entry.Level {
|
|
||||||
case DebugLevel, TraceLevel:
|
|
||||||
levelColor = gray
|
|
||||||
case WarnLevel:
|
|
||||||
levelColor = yellow
|
|
||||||
case ErrorLevel, FatalLevel, PanicLevel:
|
|
||||||
levelColor = red
|
|
||||||
case InfoLevel:
|
|
||||||
levelColor = blue
|
|
||||||
default:
|
|
||||||
levelColor = blue
|
|
||||||
}
|
|
||||||
|
|
||||||
levelText := strings.ToUpper(entry.Level.String())
|
|
||||||
if !f.DisableLevelTruncation && !f.PadLevelText {
|
|
||||||
levelText = levelText[0:4]
|
|
||||||
}
|
|
||||||
if f.PadLevelText {
|
|
||||||
// Generates the format string used in the next line, for example "%-6s" or "%-7s".
|
|
||||||
// Based on the max level text length.
|
|
||||||
formatString := "%-" + strconv.Itoa(f.levelTextMaxLength) + "s"
|
|
||||||
// Formats the level text by appending spaces up to the max length, for example:
|
|
||||||
// - "INFO "
|
|
||||||
// - "WARNING"
|
|
||||||
levelText = fmt.Sprintf(formatString, levelText)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a single newline if it already exists in the message to keep
|
|
||||||
// the behavior of logrus text_formatter the same as the stdlib log package
|
|
||||||
entry.Message = strings.TrimSuffix(entry.Message, "\n")
|
|
||||||
|
|
||||||
caller := ""
|
|
||||||
if entry.HasCaller() {
|
|
||||||
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
|
|
||||||
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
|
|
||||||
|
|
||||||
if f.CallerPrettyfier != nil {
|
|
||||||
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fileVal == "" {
|
|
||||||
caller = funcVal
|
|
||||||
} else if funcVal == "" {
|
|
||||||
caller = fileVal
|
|
||||||
} else {
|
|
||||||
caller = fileVal + " " + funcVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case f.DisableTimestamp:
|
|
||||||
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
|
|
||||||
case !f.FullTimestamp:
|
|
||||||
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
|
|
||||||
default:
|
|
||||||
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
|
|
||||||
}
|
|
||||||
for _, k := range keys {
|
|
||||||
v := data[k]
|
|
||||||
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
|
|
||||||
f.appendValue(b, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TextFormatter) needsQuoting(text string) bool {
|
|
||||||
if f.ForceQuote {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if f.QuoteEmptyFields && len(text) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if f.DisableQuote {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, ch := range text {
|
|
||||||
if !((ch >= 'a' && ch <= 'z') ||
|
|
||||||
(ch >= 'A' && ch <= 'Z') ||
|
|
||||||
(ch >= '0' && ch <= '9') ||
|
|
||||||
ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
|
|
||||||
if b.Len() > 0 {
|
|
||||||
b.WriteByte(' ')
|
|
||||||
}
|
|
||||||
b.WriteString(key)
|
|
||||||
b.WriteByte('=')
|
|
||||||
f.appendValue(b, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) {
|
|
||||||
stringVal, ok := value.(string)
|
|
||||||
if !ok {
|
|
||||||
stringVal = fmt.Sprint(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !f.needsQuoting(stringVal) {
|
|
||||||
b.WriteString(stringVal)
|
|
||||||
} else {
|
|
||||||
b.WriteString(fmt.Sprintf("%q", stringVal))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
102
vendor/github.com/sirupsen/logrus/writer.go
generated
vendored
102
vendor/github.com/sirupsen/logrus/writer.go
generated
vendored
@@ -1,102 +0,0 @@
|
|||||||
package logrus
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Writer at INFO level. See WriterLevel for details.
|
|
||||||
func (logger *Logger) Writer() *io.PipeWriter {
|
|
||||||
return logger.WriterLevel(InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriterLevel returns an io.Writer that can be used to write arbitrary text to
|
|
||||||
// the logger at the given log level. Each line written to the writer will be
|
|
||||||
// printed in the usual way using formatters and hooks. The writer is part of an
|
|
||||||
// io.Pipe and it is the callers responsibility to close the writer when done.
|
|
||||||
// This can be used to override the standard library logger easily.
|
|
||||||
func (logger *Logger) WriterLevel(level Level) *io.PipeWriter {
|
|
||||||
return NewEntry(logger).WriterLevel(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer returns an io.Writer that writes to the logger at the info log level
|
|
||||||
func (entry *Entry) Writer() *io.PipeWriter {
|
|
||||||
return entry.WriterLevel(InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriterLevel returns an io.Writer that writes to the logger at the given log level
|
|
||||||
func (entry *Entry) WriterLevel(level Level) *io.PipeWriter {
|
|
||||||
reader, writer := io.Pipe()
|
|
||||||
|
|
||||||
var printFunc func(args ...interface{})
|
|
||||||
|
|
||||||
// Determine which log function to use based on the specified log level
|
|
||||||
switch level {
|
|
||||||
case TraceLevel:
|
|
||||||
printFunc = entry.Trace
|
|
||||||
case DebugLevel:
|
|
||||||
printFunc = entry.Debug
|
|
||||||
case InfoLevel:
|
|
||||||
printFunc = entry.Info
|
|
||||||
case WarnLevel:
|
|
||||||
printFunc = entry.Warn
|
|
||||||
case ErrorLevel:
|
|
||||||
printFunc = entry.Error
|
|
||||||
case FatalLevel:
|
|
||||||
printFunc = entry.Fatal
|
|
||||||
case PanicLevel:
|
|
||||||
printFunc = entry.Panic
|
|
||||||
default:
|
|
||||||
printFunc = entry.Print
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start a new goroutine to scan the input and write it to the logger using the specified print function.
|
|
||||||
// It splits the input into chunks of up to 64KB to avoid buffer overflows.
|
|
||||||
go entry.writerScanner(reader, printFunc)
|
|
||||||
|
|
||||||
// Set a finalizer function to close the writer when it is garbage collected
|
|
||||||
runtime.SetFinalizer(writer, writerFinalizer)
|
|
||||||
|
|
||||||
return writer
|
|
||||||
}
|
|
||||||
|
|
||||||
// writerScanner scans the input from the reader and writes it to the logger
|
|
||||||
func (entry *Entry) writerScanner(reader *io.PipeReader, printFunc func(args ...interface{})) {
|
|
||||||
scanner := bufio.NewScanner(reader)
|
|
||||||
|
|
||||||
// Set the buffer size to the maximum token size to avoid buffer overflows
|
|
||||||
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), bufio.MaxScanTokenSize)
|
|
||||||
|
|
||||||
// Define a split function to split the input into chunks of up to 64KB
|
|
||||||
chunkSize := bufio.MaxScanTokenSize // 64KB
|
|
||||||
splitFunc := func(data []byte, atEOF bool) (int, []byte, error) {
|
|
||||||
if len(data) >= chunkSize {
|
|
||||||
return chunkSize, data[:chunkSize], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return bufio.ScanLines(data, atEOF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the custom split function to split the input
|
|
||||||
scanner.Split(splitFunc)
|
|
||||||
|
|
||||||
// Scan the input and write it to the logger using the specified print function
|
|
||||||
for scanner.Scan() {
|
|
||||||
printFunc(strings.TrimRight(scanner.Text(), "\r\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there was an error while scanning the input, log an error
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
entry.Errorf("Error while reading from Writer: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the reader when we are done
|
|
||||||
reader.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriterFinalizer is a finalizer function that closes then given writer when it is garbage collected
|
|
||||||
func writerFinalizer(writer *io.PipeWriter) {
|
|
||||||
writer.Close()
|
|
||||||
}
|
|
||||||
5
vendor/github.com/xmppo/go-xmpp/.travis.yml
generated
vendored
Normal file
5
vendor/github.com/xmppo/go-xmpp/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
script:
|
||||||
|
- go test
|
||||||
0
vendor/golang.org/x/crypto/LICENSE → vendor/github.com/xmppo/go-xmpp/LICENSE
generated
vendored
0
vendor/golang.org/x/crypto/LICENSE → vendor/github.com/xmppo/go-xmpp/LICENSE
generated
vendored
6
vendor/github.com/xmppo/go-xmpp/README.md
generated
vendored
Normal file
6
vendor/github.com/xmppo/go-xmpp/README.md
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
go-xmpp
|
||||||
|
=======
|
||||||
|
|
||||||
|
go xmpp library (original was written by russ cox )
|
||||||
|
|
||||||
|
[Documentation](https://godoc.org/github.com/xmppo/go-xmpp)
|
||||||
51
vendor/github.com/xmppo/go-xmpp/const.go
generated
vendored
Normal file
51
vendor/github.com/xmppo/go-xmpp/const.go
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package xmpp
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version = "0.3.1"
|
||||||
|
// FAST Mechanisms
|
||||||
|
HT_SHA_256_ENDP = "HT-SHA-256-ENDP"
|
||||||
|
HT_SHA_256_EXPR = "HT-SHA-256-EXPR"
|
||||||
|
HT_SHA_256_NONE = "HT-SHA-256-NONE"
|
||||||
|
HT_SHA_256_UNIQ = "HT-SHA-256-UNIQ"
|
||||||
|
// IQ Types
|
||||||
|
IQTypeError = "error"
|
||||||
|
IQTypeGet = "get"
|
||||||
|
IQTypeResult = "result"
|
||||||
|
IQTypeSet = "set"
|
||||||
|
// SCRAM Mechanisms
|
||||||
|
SCRAM_SHA_1_PLUS = "SCRAM-SHA-1-PLUS"
|
||||||
|
SCRAM_SHA_1 = "SCRAM-SHA-1"
|
||||||
|
SCRAM_SHA_256_PLUS = "SCRAM-SHA-256-PLUS"
|
||||||
|
SCRAM_SHA_256 = "SCRAM-SHA-256"
|
||||||
|
SCRAM_SHA_512_PLUS = "SCRAM-SHA-512-PLUS"
|
||||||
|
SCRAM_SHA_512 = "SCRAM-SHA-512"
|
||||||
|
UPGR_SCRAM_SHA_256 = "UPGR-SCRAM-SHA-256"
|
||||||
|
UPGR_SCRAM_SHA_512 = "UPGR-SCRAM-SHA-512"
|
||||||
|
// XMPP Namespaces
|
||||||
|
XMPPNS_AVATAR_PEP_DATA = "urn:xmpp:avatar:data"
|
||||||
|
XMPPNS_AVATAR_PEP_METADATA = "urn:xmpp:avatar:metadata"
|
||||||
|
XMPPNS_BIND_0 = "urn:xmpp:bind:0"
|
||||||
|
XMPPNS_CLIENT = "jabber:client"
|
||||||
|
XMPPNS_DISCO_INFO = "http://jabber.org/protocol/disco#info"
|
||||||
|
XMPPNS_DISCO_ITEMS = "http://jabber.org/protocol/disco#items"
|
||||||
|
XMPPNS_FAST_0 = "urn:xmpp:fast:0"
|
||||||
|
XMPPNS_HTTP_UPLOAD_0 = "urn:xmpp:http:upload:0"
|
||||||
|
XMPPNS_IQ_VERSION = "jabber:iq:version"
|
||||||
|
XMPPNS_MUC = "http://jabber.org/protocol/muc"
|
||||||
|
XMPPNS_MUC_USER = "http://jabber.org/protocol/muc#user"
|
||||||
|
XMPPNS_PING = "urn:xmpp:ping"
|
||||||
|
XMPPNS_PUBSUB_EVENT = "http://jabber.org/protocol/pubsub#event"
|
||||||
|
XMPPNS_PUBSUB = "http://jabber.org/protocol/pubsub"
|
||||||
|
XMPPNS_SASL_2 = "urn:xmpp:sasl:2"
|
||||||
|
XMPPNS_SASL_CB_0 = "urn:xmpp:sasl-cb:0"
|
||||||
|
XMPPNS_SASL_UPGRADE_0 = "urn:xmpp:sasl:upgrade:0"
|
||||||
|
XMPPNS_XMPP_SASL = "urn:ietf:params:xml:ns:xmpp-sasl"
|
||||||
|
XMPPNS_SCRAM_UPGRADE_0 = "urn:xmpp:scram-upgrade:0"
|
||||||
|
XMPPNS_SID_0 = "urn:xmpp:sid:0"
|
||||||
|
XMPPNS_STREAM = "http://etherx.jabber.org/streams"
|
||||||
|
XMPPNS_STREAM_LIMITS_0 = "urn:xmpp:stream-limits:0"
|
||||||
|
XMPPNS_TIME = "urn:xmpp:time"
|
||||||
|
XMPPNS_XMPP_TLS = "urn:ietf:params:xml:ns:xmpp-tls"
|
||||||
|
XMPPNS_XMPP_BIND = "urn:ietf:params:xml:ns:xmpp-bind"
|
||||||
|
XMPPNS_XMPP_SESSION = "urn:ietf:params:xml:ns:xmpp-session"
|
||||||
|
)
|
||||||
2460
vendor/github.com/xmppo/go-xmpp/xmpp.go
generated
vendored
Normal file
2460
vendor/github.com/xmppo/go-xmpp/xmpp.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user