Compare commits
19 Commits
e54144d716
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124009613e | ||
|
|
ddcc8b1c87 | ||
|
|
bb2425f404 | ||
|
|
af7ff57279 | ||
|
|
5471e09a24 | ||
|
|
b50c0612a6 | ||
|
|
1e54dcf9f1 | ||
|
|
61208f5247 | ||
|
|
fbd0196a48 | ||
|
|
c31547ce4a | ||
|
|
490677de40 | ||
|
|
d8a92b37fc | ||
|
|
c149b92c0c | ||
|
|
5a54572b91 | ||
|
|
f427dca430 | ||
|
|
2af4cdd706 | ||
|
|
0c22128c77 | ||
|
|
037b07a00d | ||
|
|
64da7d6ce7 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,9 @@
|
|||||||
.idea/**
|
.idea/**
|
||||||
.vscode/**
|
.vscode/**
|
||||||
.vstags
|
.vstags
|
||||||
data/config.json
|
/data/config.json
|
||||||
data/*_db
|
/data/*_db
|
||||||
aleesa-telegram-go
|
/data/cache
|
||||||
|
/aleesa-telegram-go
|
||||||
|
/*.log
|
||||||
|
/*.bck.*
|
||||||
|
|||||||
743
.golangci.yml
743
.golangci.yml
@@ -1,28 +1,12 @@
|
|||||||
|
version: "2"
|
||||||
run:
|
run:
|
||||||
tests: false
|
go: "1.25"
|
||||||
skip-dirs:
|
|
||||||
- data
|
|
||||||
- vendor
|
|
||||||
modules-download-mode: readonly
|
modules-download-mode: readonly
|
||||||
|
tests: false
|
||||||
allow-parallel-runners: true
|
allow-parallel-runners: true
|
||||||
go: '1.21'
|
|
||||||
output:
|
|
||||||
sort-results: true
|
|
||||||
linters-settings:
|
|
||||||
gosec:
|
|
||||||
excludes:
|
|
||||||
# Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки
|
|
||||||
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
|
|
||||||
- G404 # Insecure random number source (rand)
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
enable:
|
||||||
- errcheck
|
|
||||||
- gosimple
|
|
||||||
- govet
|
|
||||||
- ineffassign
|
|
||||||
- staticcheck
|
|
||||||
- unused
|
|
||||||
- asasalint
|
- asasalint
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- bodyclose
|
- bodyclose
|
||||||
@@ -30,24 +14,30 @@ linters:
|
|||||||
- contextcheck
|
- contextcheck
|
||||||
- decorder
|
- decorder
|
||||||
- durationcheck
|
- durationcheck
|
||||||
|
- errcheck
|
||||||
- errchkjson
|
- errchkjson
|
||||||
- errorlint
|
- errorlint
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
- exptostd
|
||||||
- gci
|
|
||||||
- ginkgolinter
|
- ginkgolinter
|
||||||
- gocheckcompilerdirectives
|
- gocheckcompilerdirectives
|
||||||
- gocritic
|
- gocritic
|
||||||
|
- godoclint
|
||||||
- godot
|
- godot
|
||||||
- gofmt
|
- godox
|
||||||
- goimports
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosmopolitan
|
- gosmopolitan
|
||||||
|
- govet
|
||||||
- grouper
|
- grouper
|
||||||
- importas
|
- importas
|
||||||
|
- ineffassign
|
||||||
|
- intrange
|
||||||
- ireturn
|
- ireturn
|
||||||
- makezero
|
- makezero
|
||||||
- mirror
|
- mirror
|
||||||
|
- misspell
|
||||||
|
- modernize
|
||||||
- nakedret
|
- nakedret
|
||||||
- nilerr
|
- nilerr
|
||||||
- nilnil
|
- nilnil
|
||||||
@@ -55,13 +45,714 @@ linters:
|
|||||||
- noctx
|
- noctx
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- nonamedreturns
|
- nonamedreturns
|
||||||
|
- perfsprint
|
||||||
- predeclared
|
- predeclared
|
||||||
- promlinter
|
- promlinter
|
||||||
- reassign
|
- reassign
|
||||||
- revive
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- stylecheck
|
- staticcheck
|
||||||
- unconvert
|
- unconvert
|
||||||
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
- wsl
|
- 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
|
||||||
|
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$
|
||||||
|
|||||||
99
Makefile
99
Makefile
@@ -1,20 +1,109 @@
|
|||||||
#!/usr/bin/env gmake -f
|
#!/usr/bin/env gmake -f
|
||||||
|
|
||||||
GOOPTS=CGO_ENABLED=0
|
GOOPTS=CGO_ENABLED=0
|
||||||
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath
|
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
|
||||||
|
MYNAME1=aleesa-telegram-go
|
||||||
|
BINARY1=${MYNAME1}
|
||||||
|
UNIX_BINARY1=${MYNAME1}
|
||||||
|
WINDOWS_BINARY1=${MYNAME1}.exe
|
||||||
|
MYNAME2=settings-migrator
|
||||||
|
BINARY2=${MYNAME2}
|
||||||
|
UNIX_BINARY2=${MYNAME2}
|
||||||
|
WINDOWS_BINARY2=${MYNAME2}.exe
|
||||||
|
RMCMD=rm -rf
|
||||||
|
|
||||||
|
# On windows binary name can depend not only on platform selected, but target too, we need no .exe suffix for linux bin.
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
ifdef GOOS
|
||||||
|
ifeq ($(GOOS),windows)
|
||||||
|
BINARY1=${WINDOWS_BINARY1}
|
||||||
|
BINARY2=${WINDOWS_BINARY2}
|
||||||
|
else # not ifeq ($(GOOS),windows)
|
||||||
|
BINARY1=${MYNAME1}
|
||||||
|
BINARY2=${MYNAME2}
|
||||||
|
endif # ifeq ($(GOOS),windows)
|
||||||
|
else # not ifdef GOOS
|
||||||
|
BINARY1=${WINDOWS_BINARY1}
|
||||||
|
BINARY2=${WINDOWS_BINARY2}
|
||||||
|
endif # ifdef GOOS
|
||||||
|
ifeq ($(SHELL), sh.exe)
|
||||||
|
RMCMD=DEL /Q /F
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Set newline symbol explicitly to aboud undefined behaviour in windows.
|
||||||
|
define IFS
|
||||||
|
|
||||||
|
endef
|
||||||
|
|
||||||
|
|
||||||
all: clean build
|
all: clean build
|
||||||
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
${GOOPTS} go build ${BUILDOPTS} -o aleesa-telegram-go
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
|
||||||
|
ifeq ($(SHELL), sh.exe)
|
||||||
|
# # Vanilla cmd.exe / powershell.
|
||||||
|
SET "CGO_ENABLED=0"
|
||||||
|
go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1}
|
||||||
|
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
# # git-bash
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1}
|
||||||
|
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
# # Some other shell.
|
||||||
|
# # TODO: handle it.
|
||||||
|
$(info "-- Dunno how to handle this shell: ${SHELL}")
|
||||||
|
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
else # not ($(OS),Windows_NT)
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1}
|
||||||
|
endif # ifeq ($(OS),Windows_NT)
|
||||||
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
go clean
|
ifeq ($(OS),Windows_NT)
|
||||||
|
ifeq ($(SHELL),sh.exe)
|
||||||
|
# # Vanilla cmd.exe / powershell.
|
||||||
|
if exist ${WINDOWS_BINARY1} ${RMCMD} ${WINDOWS_BINARY1}
|
||||||
|
if exist ${UNIX_BINARY1} ${RMCMD} ${UNIX_BINARY1}
|
||||||
|
if exist ${WINDOWS_BINARY2} ${RMCMD} ${WINDOWS_BINARY2}
|
||||||
|
if exist ${UNIX_BINARY2} ${RMCMD} ${UNIX_BINARY2}
|
||||||
|
else # not ifeq ($(SHELL),sh.exe)
|
||||||
|
${RMCMD} ./${WINDOWS_BINARY1}
|
||||||
|
${RMCMD} ./${UNIX_BINARY1}
|
||||||
|
${RMCMD} ./${WINDOWS_BINARY2}
|
||||||
|
${RMCMD} ./${UNIX_BINARY2}
|
||||||
|
endif # ifeq ($(SHELL),sh.exe)
|
||||||
|
else # not ifeq ($(OS),Windows_NT)
|
||||||
|
${RMCMD} ./${UNIX_BINARY1}
|
||||||
|
${RMCMD} ./${UNIX_BINARY2}
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
upgrade:
|
upgrade:
|
||||||
rm -rf vendor
|
go get -u ./...
|
||||||
go get -d -u -t ./...
|
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go mod vendor
|
go mod vendor
|
||||||
|
|
||||||
|
|
||||||
|
# Build settings-migrator binary.
|
||||||
|
${MYNAME2}: clean
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
|
||||||
|
ifeq ($(SHELL), sh.exe)
|
||||||
|
# # Vanilla cmd.exe / powershell.
|
||||||
|
SET "CGO_ENABLED=0"
|
||||||
|
go build ${BUILDOPTS} -o ${WINDOWS_BINARY2} ./cmd/${MYNAME2}
|
||||||
|
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
# # git-bash
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${WINDOWS_BINARY2} ./cmd/${MYNAME2}
|
||||||
|
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
# # Some other shell.
|
||||||
|
# # TODO: handle it.
|
||||||
|
$(info "-- Dunno how to handle this shell: ${SHELL}")
|
||||||
|
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
|
||||||
|
else # not ($(OS),Windows_NT)
|
||||||
|
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${UNIX_BINARY2} ./cmd/${MYNAME2}
|
||||||
|
endif # ifeq ($(OS),Windows_NT)
|
||||||
|
|
||||||
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
# vim: set ft=make noet ai ts=4 sw=4 sts=4:
|
||||||
|
|||||||
36
TODO.md
36
TODO.md
@@ -1,36 +0,0 @@
|
|||||||
# Перечень того, что надо реализовать.
|
|
||||||
|
|
||||||
В качестве основы возьмём https://github.com/NicoNex/echotron
|
|
||||||
|
|
||||||
Для реализации телеграммного фронтэнда нам надо сделать:
|
|
||||||
|
|
||||||
0. Парсер конфига.
|
|
||||||
[V] Реализован начальный вариант парсера конфига.
|
|
||||||
|
|
||||||
1. Разгребальщик апдейтов и формирователь redis-ных сообщений.
|
|
||||||
[ ] Написана заглушка для парсера апдейтов bot api телеграма
|
|
||||||
|
|
||||||
2. Разгребальщик редисных сообщений и формирователь запросов в tg bot api.
|
|
||||||
[ ] Написан черновой вариант рагребателя
|
|
||||||
|
|
||||||
3. Обработчик группы команд admin, сохранение настроек в pebbledb.
|
|
||||||
|
|
||||||
1 цензор сообщений определённого типа.
|
|
||||||
|
|
||||||
2 фортунка каждое утро. (+ добавить время, относительно gmt, когда показывать фортунку).
|
|
||||||
|
|
||||||
3 Приветствие новых участников
|
|
||||||
|
|
||||||
4 oboobs
|
|
||||||
|
|
||||||
5 obutts
|
|
||||||
|
|
||||||
6 chan_msg
|
|
||||||
|
|
||||||
7 user ban
|
|
||||||
|
|
||||||
8 user mute
|
|
||||||
|
|
||||||
4. help
|
|
||||||
|
|
||||||
5. Readme.txt
|
|
||||||
73
TODO.org
Normal file
73
TODO.org
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
Перечень того, что надо реализовать.
|
||||||
|
|
||||||
|
В качестве основы возьмём https://github.com/NicoNex/echotron
|
||||||
|
|
||||||
|
Для реализации телеграммного фронтэнда нам надо сделать:
|
||||||
|
* DONE Переделать layout [1/1]
|
||||||
|
- [X] На гошный
|
||||||
|
|
||||||
|
* DONE Парсер конфига. [1/1]
|
||||||
|
- [X] Реализован начальный вариант парсера конфига.
|
||||||
|
|
||||||
|
* DONE Разгребальщик апдейтов и формирователь redis-ных сообщений. [2/2]
|
||||||
|
- [X] Написана заглушка для парсера апдейтов bot api телеграма
|
||||||
|
- [X] Написан парсер прочих команд
|
||||||
|
|
||||||
|
* TODO Разгребальщик редисных сообщений и формирователь запросов в tg bot api. [1/2]
|
||||||
|
- [X] Написан черновой вариант разгребателя
|
||||||
|
- [ ] Написан мигратор для группового чата. (апгрейд до супергруппы, до чата с тредами)
|
||||||
|
|
||||||
|
* TODO Обработчик группы команд admin, сохранение настроек в pebbledb. [5/9]
|
||||||
|
- [X] [X] цензор сообщений определённого типа.
|
||||||
|
- [X] [X] фортунка каждое утро. См. п.13.
|
||||||
|
- [X] [X] Приветствие новых участников
|
||||||
|
- [X] [X] Прощание с теми, кто ушёл
|
||||||
|
- [ ] [ ] oboobs
|
||||||
|
- [ ] [ ] obutts
|
||||||
|
- [X] [X] chan_msg (Пока что бот сам удяляет сообщения от таких имперсонализированных пользователей, однако в прроде
|
||||||
|
есть возможность таких персонажей банить, см banChatSenderChat()/unbanChatSenderChat()
|
||||||
|
https://core.telegram.org/bots/api#banchatsenderchat, надо ли реализовывать такой бан - над этим надо ещё
|
||||||
|
подумать.)
|
||||||
|
- [ ] [ ] user ban
|
||||||
|
- [ ] [ ] user mute
|
||||||
|
|
||||||
|
* DONE help [1/1]
|
||||||
|
- [X] done
|
||||||
|
|
||||||
|
* DONE help admin [1/1]
|
||||||
|
- [X] done
|
||||||
|
|
||||||
|
* TODO Написать README.md [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Поддержать миграцию групп в супергруппы (итд, там меняется id и надо перекидывать настройки на новый id) [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Поддержать парсинг ошибок отправки сообщений в телегу (в т.ч. мигратор для группового чата, также внятные сообщения в лог, если бот замьючен) [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Поддержать leaveChat(), чтобы бот мог легально и самостоятельно покинуть чят. [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Cтавить реакцию thumbsup на спасибо, пожалуйста итд, ставить сердечко, если похвалили - молодец, красава итд. (setMessageReaction() на соответствующий ответ на сообщение) [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Отслеживать участников чятика, чтобы была возможность мьютить, банить, узнавать инфу по разным участникам чятика. [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Промоут/демоут участника до админа [0/1]
|
||||||
|
- [ ] done
|
||||||
|
|
||||||
|
* TODO Cron-like "сервис". Он нужен для [2/3]
|
||||||
|
- [X] отправки приветствий по утрам.
|
||||||
|
- [X] регулярных компакций базы с настройками.
|
||||||
|
|
||||||
|
* TODO Нужен скрипт для выгрузки настроек из перловой базы и имортёр настроек в базу настроек нового бота [0/2]
|
||||||
|
- [ ] Написать перловый скрипт, сохраняющий настройки чатов в settings.json
|
||||||
|
- [ ] Написать гошную программу для импорта настроек из settings.json в базу гошного бота
|
||||||
|
|
||||||
|
* DONE Усложнить Makefile [1/1]
|
||||||
|
- [X] сделать его его кроссплатформенным, по примеру других гошных приложений
|
||||||
|
|
||||||
|
# -*- mode: org; -*-
|
||||||
|
# vim: ft=org
|
||||||
82
cmd/aleesa-telegram-go/main.go
Normal file
82
cmd/aleesa-telegram-go/main.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Package main is the main package of aleesa-telegram-go.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
"aleesa-telegram-go/internal/tg"
|
||||||
|
|
||||||
|
"github.com/carlescere/scheduler"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
logfile *os.File
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Найдём и прочитаем конфиг.
|
||||||
|
tg.ReadConfig()
|
||||||
|
|
||||||
|
// Откроем лог и скормим его логгеру.
|
||||||
|
if tg.Config.Log != "" {
|
||||||
|
logfile, err = os.OpenFile(tg.Config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to open log file %s: %s", tg.Config.Log, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Init(tg.Config.Loglevel, logfile)
|
||||||
|
|
||||||
|
// Инициализируем redis-клиента.
|
||||||
|
tg.RedisClient = redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", tg.Config.Redis.Server, tg.Config.Redis.Port),
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Debugf("Lazy connect() to redis at %s:%d", tg.Config.Redis.Server, tg.Config.Redis.Port)
|
||||||
|
|
||||||
|
tg.Subscriber = tg.RedisClient.Subscribe(tg.Ctx, tg.Config.Redis.MyChannel)
|
||||||
|
|
||||||
|
redisMsgChan := tg.Subscriber.Channel()
|
||||||
|
|
||||||
|
// Самое время поставить траппер сигналов.
|
||||||
|
signal.Notify(tg.SigChan,
|
||||||
|
syscall.SIGINT,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
syscall.SIGQUIT)
|
||||||
|
|
||||||
|
go tg.SigHandler()
|
||||||
|
go tg.Telega(tg.Config)
|
||||||
|
|
||||||
|
// Периодически обслуживаем базы с настройками чатов.
|
||||||
|
if job, err := scheduler.Every(1).Hours().NotImmediately().Run(tg.TidySettingsDB); err != nil {
|
||||||
|
log.Errorf("Unable to schedule periodic settings db flush: %s", err)
|
||||||
|
} else {
|
||||||
|
tg.PeriodicJobs = append(tg.PeriodicJobs, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправлем "доброе утро" каждое утро для всех чатов, которые на это подписались.
|
||||||
|
if job, err := scheduler.Every().Day().At("8:10").Run(tg.SendGoodMorning); err != nil {
|
||||||
|
log.Errorf("Unable to schedule send good morning task: %s", err)
|
||||||
|
} else {
|
||||||
|
tg.PeriodicJobs = append(tg.PeriodicJobs, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик событий от редиски.
|
||||||
|
for msg := range redisMsgChan {
|
||||||
|
if tg.Shutdown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tg.RedisMsgParser(msg.Payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
14
cmd/settings-migrator/exporter.pl
Normal file
14
cmd/settings-migrator/exporter.pl
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use English;
|
||||||
|
use utf8;
|
||||||
|
|
||||||
|
use JSON::XS qw (encode_json);
|
||||||
|
|
||||||
|
# FIXME: написать настоящий экспортёр настроек!
|
||||||
|
print "Hello world!\n";
|
||||||
|
|
||||||
|
# vim: set ft=perl noet ai ts=4 sw=4 sts=4:
|
||||||
13
cmd/settings-migrator/importer.go
Normal file
13
cmd/settings-migrator/importer.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// main Импортирует настройки из файла settings.json в базу с настройками.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FIXME: нписать настоящий импортер настроек!
|
||||||
|
func main() {
|
||||||
|
fmt.Print("Hello world!\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
{
|
{
|
||||||
# Cachedir used not only for caches but for some settings and stats too.
|
# Data_dir used not only for caches but for some settings and stats too.
|
||||||
"cachedir" : "data/cache",
|
"data_dir" : "data",
|
||||||
|
|
||||||
# Required for debugging reasons, if commented out or not exist, no debug logging appears at all.
|
# Required for debugging reasons, if commented out or not exist, no debug logging appears at all.
|
||||||
"log" : "/var/log/aleesa-telegram/debug.log",
|
"log" : "/var/log/aleesa-telegram/debug.log",
|
||||||
|
|
||||||
# Verbosity can be trace, debug, info (inform), notice, warning (warn), error (err), critical (crit, fatal), alert, emergency.
|
# Verbosity can be trace, debug, info, warn, error.
|
||||||
# Currently implemented debug, info, notice, warn, error, fatal. Notice is default logging level.
|
# Currently implemented debug, info, warn, error. Info is default logging level.
|
||||||
"loglevel": "notice",
|
"loglevel": "info",
|
||||||
|
|
||||||
# Stuff for telegram itself.
|
# Stuff for telegram itself.
|
||||||
"telegrambot" : {
|
"telegram" : {
|
||||||
# Prefix-sign for all commands. In many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!".
|
"token" : "Long_string_given_by_@BotFather"
|
||||||
# In telegram commands begin with "/" on the start of first (and only) line. In case of this bot you can define what to use.
|
},
|
||||||
"csign" : "!",
|
|
||||||
|
|
||||||
"token" : "Long_string_given_by_@BotFather"
|
# Prefix-sign for all commands. In many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!".
|
||||||
},
|
# In telegram commands begin with "/" on the start of first (and only) line. In case of this bot you can define what to use.
|
||||||
|
"csign" : "!",
|
||||||
|
|
||||||
# Unprivileged user to run this bot from.
|
# Unprivileged user to run this bot from.
|
||||||
"user" : "nobody",
|
"user" : "nobody",
|
||||||
|
|
||||||
"redis_server": "redis",
|
"redis": {
|
||||||
"redis_port" : "6379",
|
"server": "redis",
|
||||||
"redis_channels" : [
|
"port" : 6379,
|
||||||
"telegram"
|
"channel" : "misc",
|
||||||
],
|
"my_channel": "telegram"
|
||||||
|
}
|
||||||
"redis_router_channel" : "misc"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vim: set ft=json noet ai ts=4 sw=4 sts=4:
|
||||||
|
|||||||
45
globals.go
45
globals.go
@@ -1,45 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/NicoNex/echotron/v3"
|
|
||||||
"github.com/cockroachdb/pebble"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config - это у нас глобальная штука.
|
|
||||||
var config myConfig
|
|
||||||
|
|
||||||
// To break circular message forwarding we must set some sane default, it can be overridden via config.
|
|
||||||
var forwardMax int64 = 5
|
|
||||||
|
|
||||||
// Объектики клиента-редиски.
|
|
||||||
var redisClient *redis.Client
|
|
||||||
var subscriber *redis.PubSub
|
|
||||||
|
|
||||||
// Main context.
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
// Ставится в true, если мы получили сигнал на выключение.
|
|
||||||
var shutdown = false
|
|
||||||
|
|
||||||
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
|
||||||
var sigChan = make(chan os.Signal, 1)
|
|
||||||
|
|
||||||
// Мапка с открытыми дескрипторами баз с настройками.
|
|
||||||
var settingsDB = make(map[string]*pebble.DB)
|
|
||||||
|
|
||||||
var tg echotron.API
|
|
||||||
|
|
||||||
// Приветсвенные фразы для новых участников чата.
|
|
||||||
var introduceGreet = [...]string{
|
|
||||||
"Дратути",
|
|
||||||
"Дарована",
|
|
||||||
"Доброе утро, день или вечер",
|
|
||||||
"Добро пожаловать в наше скромное коммунити",
|
|
||||||
"Наше вам с кисточкой тут, на канальчике",
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
4
go.mod
4
go.mod
@@ -1,15 +1,15 @@
|
|||||||
module aleesa-telegram-go
|
module aleesa-telegram-go
|
||||||
|
|
||||||
go 1.22
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/NicoNex/echotron/v3 v3.33.1
|
github.com/NicoNex/echotron/v3 v3.33.1
|
||||||
|
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
|
||||||
github.com/cockroachdb/errors v1.11.3
|
github.com/cockroachdb/errors v1.11.3
|
||||||
github.com/cockroachdb/pebble v1.1.1
|
github.com/cockroachdb/pebble v1.1.1
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -4,6 +4,8 @@ github.com/NicoNex/echotron/v3 v3.33.1 h1:P9bVvmnuVd0C7bR5q9TV6Pece/wBqZ6PSo0Rqo
|
|||||||
github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
|
github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI=
|
||||||
|
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
||||||
@@ -21,7 +23,6 @@ github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZ
|
|||||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
@@ -76,10 +77,6 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG
|
|||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
@@ -105,7 +102,6 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -124,11 +120,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
177
internal/log/log.go
Normal file
177
internal/log/log.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// Package log wraps log/slog for more easier and convinient use.
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
var loglevel slog.Level
|
||||||
|
|
||||||
|
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 the String. The string has correct time format.
|
||||||
|
a.Value = slog.StringValue(t.Format(time.DateTime))
|
||||||
|
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
|
||||||
|
Level: loglevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler = slog.NewTextHandler(fileDescriptor, opts)
|
||||||
|
|
||||||
|
slog.SetDefault(
|
||||||
|
slog.New(Handler),
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
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...))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
501
internal/tg/admin.go
Normal file
501
internal/tg/admin.go
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
// Package tg implements most of aleesa-telegram-go functionality.
|
||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Admin парсит команды семейства admin. Возвращает true, если команда была опознана как admin.
|
||||||
|
func Admin(msg *echotron.Update) (bool, error) {
|
||||||
|
var (
|
||||||
|
c = msg.Message.Text[len(Config.Csign):]
|
||||||
|
answer string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == "admin" || c == "админ":
|
||||||
|
answer = "```\n"
|
||||||
|
|
||||||
|
answer += Config.Csign + "admin censor - показать список состояния типов сообщений\n"
|
||||||
|
answer += Config.Csign + "админ ценз - показать список состояния типов сообщений\n"
|
||||||
|
answer += Config.Csign + "admin censor type # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n"
|
||||||
|
answer += Config.Csign + "админ ценз тип # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n"
|
||||||
|
answer += Config.Csign + "admin fortune - показываем ли с утра фортунку для чата\n"
|
||||||
|
answer += Config.Csign + "admin фортунка - показываем ли с утра фортунку для чата\n"
|
||||||
|
answer += Config.Csign + "admin fortune # - где 1 - вкл, 0 - выкл фортунку с утра\n"
|
||||||
|
answer += Config.Csign + "admin фортунка # - где 1 - вкл, 0 - выкл фортунку с утра\n"
|
||||||
|
answer += Config.Csign + "admin greet - приветствуем ли новых участников чата\n"
|
||||||
|
answer += Config.Csign + "admin приветствие - приветствуем ли новых участников чата\n"
|
||||||
|
answer += Config.Csign + "admin greet # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n"
|
||||||
|
answer += Config.Csign + "admin приветствие # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n"
|
||||||
|
answer += Config.Csign + "admin goodbye - прощаемся ли с ушедшими участниками чата\n"
|
||||||
|
answer += Config.Csign + "admin прощание - прощаемся ли с ушедшими участниками чата\n"
|
||||||
|
answer += Config.Csign + "admin goodbye # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n"
|
||||||
|
answer += Config.Csign + "admin прощание # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n"
|
||||||
|
answer += Config.Csign + "admin oboobs # - где 1 - вкл, 0 - выкл плагина oboobs\n"
|
||||||
|
|
||||||
|
answer += fmt.Sprintf(
|
||||||
|
"%sadmin oboobs - показываем ли сисечки по просьбе участников чата (команды %stits, %stities, %sboobs, %sboobies, %sсиси, %sсисечки)\n",
|
||||||
|
Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer += Config.Csign + "admin obutts # - где 1 - вкл, 0 - выкл плагина obutts\n"
|
||||||
|
|
||||||
|
answer += fmt.Sprintf(
|
||||||
|
"%sadmin obutts - показываем ли попки по просьбе участников чата (команды %sass, %sbutt, %sbooty, %sпопа, %sпопка)\n",
|
||||||
|
Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer += Config.Csign + "admin chan_msg - оставляем ли сообщения присланные от имени (других) каналов\n"
|
||||||
|
answer += Config.Csign + "admin chan_msg # - где 1 - оставляем, 0 - удаляем\n"
|
||||||
|
answer += Config.Csign + "admin ban userid sec - выдаём ban указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n"
|
||||||
|
answer += Config.Csign + "admin mute userid sec - выдаём mute указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n"
|
||||||
|
answer += Config.Csign + "admin admin mute - разрешено ли обычным админам мьютить участников чата через бота (если бот - админ), (создатель чата всегда может попросить бота-админа замьютить обычного участника чата)\n"
|
||||||
|
|
||||||
|
answer += Config.Csign + "%sadmin admin mute # - где 1 - разрешено, 0 - не разрешено\n"
|
||||||
|
answer += "\n"
|
||||||
|
answer += "Типы сообщений:\naudio voice photo video video_note animation sticker dice game poll document\n"
|
||||||
|
answer += "```"
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)$").MatchString(c):
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg") == "1" {
|
||||||
|
answer += "Тип сообщения voice удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения voice не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg") == "1" {
|
||||||
|
answer += "Тип сообщения audio удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения audio не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg") == "1" {
|
||||||
|
answer += "Тип сообщения photo удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения photo не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg") == "1" {
|
||||||
|
answer += "Тип сообщения video удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения video не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg") == "1" {
|
||||||
|
answer += "Тип сообщения video_note удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения video_note не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg") == "1" {
|
||||||
|
answer += "Тип сообщения animation удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения animation не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg") == "1" {
|
||||||
|
answer += "Тип сообщения sticker удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения sticker не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DiceMsg") == "1" {
|
||||||
|
answer += "Тип сообщения dice удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения dice не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg") == "1" {
|
||||||
|
answer += "Тип сообщения game удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения game не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg") == "1" {
|
||||||
|
answer += "Тип сообщения poll удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения poll не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg") == "1" {
|
||||||
|
answer += "Тип сообщения document удаляется\n"
|
||||||
|
} else {
|
||||||
|
answer += "Тип сообщения document не удаляется\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)[[:space:]].+$").MatchString(c):
|
||||||
|
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||||
|
// Выхватываем тип сообщения и согласно типу ставим настройку.
|
||||||
|
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 4)
|
||||||
|
|
||||||
|
switch cmdArray[2] {
|
||||||
|
case "voice":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с voice будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с voice будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "audio":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с audio будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с audio будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "photo":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с photo будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с photo будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с video будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с video будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "video_note":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с video_note будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с video_note будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "animation":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с animation будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с animation будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "sticker":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с sticker будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с sticker будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "dice":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DiceMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с dice будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "diceMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с dice будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "game":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с game будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с game будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "poll":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с poll будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с poll будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case "document":
|
||||||
|
switch cmdArray[3] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с document будут оставаться"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с document будут удаляться"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)$").MatchString(c):
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg") == "1" {
|
||||||
|
answer += "Фортунки с утра показываются"
|
||||||
|
} else {
|
||||||
|
answer += "Фортунки с утра не показываются"
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)[[:space:]].*$").MatchString(c):
|
||||||
|
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||||
|
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||||
|
|
||||||
|
// TODO: На самом деле надо сохранять список чятиков, в которые надо отправлять сообщения. Из этой базы
|
||||||
|
// мы не сможем вытащить список чятиков.
|
||||||
|
switch cmdArray[2] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с фортункой будут отправляться с утра"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Теперь сообщения с фортункой не будут отправляться с утра"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)$").MatchString(c):
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg") == "1" {
|
||||||
|
answer += "Приветствуем вновьприбывших участников чатика"
|
||||||
|
} else {
|
||||||
|
answer += "Не приветствуем вновьприбывших участников чатика"
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)[[:space:]].*$").MatchString(c):
|
||||||
|
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||||
|
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||||
|
|
||||||
|
// TODO: На самом деле надо сохранять список чятиков, в которые надо отправлять сообщения. Из этой базы
|
||||||
|
// мы не сможем вытащить список чятиков.
|
||||||
|
switch cmdArray[2] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Будем приветствовать вновьприбывших участников чата"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Не будем приветствовать вновьприбывших участников чата"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю, о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)$").MatchString(c):
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg") == "1" {
|
||||||
|
answer += "Прощаемся с ушедшими участниками чатика"
|
||||||
|
} else {
|
||||||
|
answer += "Не прощаемся с ушедшими участниками чатика"
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)[[:space:]].*$").MatchString(c):
|
||||||
|
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||||
|
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||||
|
|
||||||
|
switch cmdArray[2] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Будем прощаться с ушедшими участниками чата"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Не будем прощаться с ушедшими участниками чата"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю, о чём ты."
|
||||||
|
}
|
||||||
|
|
||||||
|
case regexp.MustCompile("^(admin|админ)[[:space:]](chan_msg)[[:space:]].*$").MatchString(c):
|
||||||
|
// Снова регулярка, это не оч эффективно, но зато просто.
|
||||||
|
cmdArray := regexp.MustCompile("[[:space:]]").Split(c, 3)
|
||||||
|
|
||||||
|
switch cmdArray[2] {
|
||||||
|
case "1":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "ChanMsg", "1"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Будем удалять сообщения, написанные от имени канала"
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "ChanMsg", "0"); err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answer += "Не будем удалять сообщения, написанные от имени канала"
|
||||||
|
|
||||||
|
default:
|
||||||
|
answer += "Не понимаю, о чём ты."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := tg.SendMessage(
|
||||||
|
answer,
|
||||||
|
msg.Message.Chat.ID,
|
||||||
|
&echotron.MessageOptions{ParseMode: "MarkdownV2"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil || !resp.Ok {
|
||||||
|
// TODO: поддержать миграцию группы в супергруппу, поддержать вариант, когда бот замьючен.
|
||||||
|
// Красиво оформить ошибку, с полями итд, как tracedump, только ошибка.
|
||||||
|
// N.B. тут может быть сообщение о том, что группа превратилась в супергруппу, или что бот не имеет прав писать
|
||||||
|
// сообщения в чятик. Это надо бы отслеживать и хэндлить.
|
||||||
|
return true, fmt.Errorf(
|
||||||
|
"unable to send message to telegram api: %w\nResponse dump: %s",
|
||||||
|
err,
|
||||||
|
spew.Sdump(resp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
124
internal/tg/censor.go
Normal file
124
internal/tg/censor.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Censor парсит сообщения в поисках непотребных данных и если он их находит, то сообщение удаляется.
|
||||||
|
// Непотребными могут быть аудиосообщения, аудиофайлы, видеосообщения, сообщения от имени других каналов итп.
|
||||||
|
// Это могут настроить админы чятика через команду !admin censor.
|
||||||
|
func Censor(msg *echotron.Update) bool {
|
||||||
|
var (
|
||||||
|
result bool
|
||||||
|
chatID = msg.ChatID()
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "VoiceMsg") == "1":
|
||||||
|
result = true
|
||||||
|
|
||||||
|
// Предполагаем, что у voice-ов здесь всегда не ноль.
|
||||||
|
if msg.Message.Voice.Duration != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "AudioMsg") == "1":
|
||||||
|
// Предполагаем, что у аудио здесь всегда не ноль.
|
||||||
|
if msg.Message.Audio.Duration != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "PhotoMsg") == "1":
|
||||||
|
// Обычное сообщение не содержит фоток.
|
||||||
|
if len(msg.Message.Photo) != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "VideoMsg") == "1":
|
||||||
|
// Предполагаем, что у видео здесь всегда не ноль.
|
||||||
|
if msg.Message.Video.Duration != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "VideoNoteMsg") == "1":
|
||||||
|
// Предполагаем, что у видео-заметки здесь всегда не ноль.
|
||||||
|
if msg.Message.VideoNote.Duration != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "AnimationMsg") == "1":
|
||||||
|
// Предполагаем, что у анимации здесь всегда не ноль.
|
||||||
|
if msg.Message.Animation.Duration != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "StickerMsg") == "1":
|
||||||
|
// Предполагаем, что FileID не пустое только у стикера.
|
||||||
|
if msg.Message.Sticker.FileID != "" {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "DiceMsg") == "1":
|
||||||
|
// Предполагаем, что Value > 0 только у дайса.
|
||||||
|
if msg.Message.Dice.Value != 0 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "GameMsg") == "1":
|
||||||
|
// Предполагаем, что title только у game-а.
|
||||||
|
if msg.Message.Game.Title != "" {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "PollMsg") == "1":
|
||||||
|
// Предполагаем, что title только у game-а.
|
||||||
|
if msg.Message.Poll.Question != "" {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "DocumentMsg") == "1":
|
||||||
|
// Предполагаем, что FileID есть только у document-а.
|
||||||
|
if msg.Message.Document.FileID != "" {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Некоторые рекламные товарищи пытаются срать своими каналами в чятик это тоже можно зацензурить ботом и это
|
||||||
|
// пидорство он будет удалять asap.
|
||||||
|
// 136817688 - это специальный id пользователя, который принимает облик канала, на него можно нажать и попасть
|
||||||
|
// на рекламируемый канал.
|
||||||
|
case GetSetting(strconv.FormatInt(chatID, 10), "ChanMsg") == "1":
|
||||||
|
if msg.Message.From.ID == 136817688 {
|
||||||
|
delMsg(msg)
|
||||||
|
|
||||||
|
result = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
136
internal/tg/cmdParser.go
Normal file
136
internal/tg/cmdParser.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmdParser разбирает сообщение как команду и засылает его в роутер, попутно обкладывая корректными значениями
|
||||||
|
// параметров. Возвращает true если это была команда.
|
||||||
|
func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
smsg sMsg
|
||||||
|
bingo bool
|
||||||
|
)
|
||||||
|
|
||||||
|
smsg.From = "telegram"
|
||||||
|
smsg.Plugin = "telegram"
|
||||||
|
smsg.Chatid = strconv.FormatInt(cmd.Message.Chat.ID, 10)
|
||||||
|
smsg.Userid = strconv.FormatInt(cmd.Message.From.ID, 10)
|
||||||
|
smsg.Message = cmd.Message.Text
|
||||||
|
smsg.Misc.Answer = 1
|
||||||
|
|
||||||
|
// Тоже самое можно проставить выковырнув значение из cmd.Message.Chat.Type, где group/supergroup/channel - это
|
||||||
|
// public, а остальное - private, но по id проще/быстрее, хоть это и хак.
|
||||||
|
if cmd.Message.Chat.ID >= 0 {
|
||||||
|
smsg.Mode = "private"
|
||||||
|
} else {
|
||||||
|
smsg.Mode = "public"
|
||||||
|
}
|
||||||
|
|
||||||
|
smsg.Misc.Botnick = ConstructPartialUserUsername(me.Result)
|
||||||
|
|
||||||
|
// Предполагаем, что длина cmd.Message.Text всегда больше или равна длине Config.Csign.
|
||||||
|
if cmd.Message.Text[0:len(Config.Csign)] == Config.Csign {
|
||||||
|
// Повторно проверяем, что текст является простой командой.
|
||||||
|
command := cmd.Message.Text[len(Config.Csign):]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case command == "помощь" || command == "help":
|
||||||
|
return Help(cmd)
|
||||||
|
|
||||||
|
// Хэндлер команд admin и admin *.
|
||||||
|
case regexp.MustCompile("^(admin|админ)(.+)?$").MatchString(command):
|
||||||
|
return Admin(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Команды в одно слово.
|
||||||
|
cmds := []string{
|
||||||
|
"ping", "пинг", "пинх", "pong", "понг", "понх", "coin", "монетка", "roll", "dice", "кости", "ver",
|
||||||
|
"version", "версия", "хэлп", "halp", "kde", "кде", "lat", "лат", "friday", "пятница", "proverb",
|
||||||
|
"пословица", "пословиться", "fortune", "фортунка", "f", "ф", "anek", "анек", "анекдот", "buni", "cat",
|
||||||
|
"кис", "drink", "праздник", "fox", "лис", "frog", "лягушка", "horse", "лошадь", "лошадка", "monkeyuser",
|
||||||
|
"owl", "сова", "сыч", "rabbit", "bunny", "кролик", "snail", "улитка", "tits", "boobs", "tities", "boobies",
|
||||||
|
"сиси", "сисечки", "butt", "booty", "ass", "попа", "попка", "xkcd", "dig", "копать", "fish", "fishing",
|
||||||
|
"рыба", "рыбка", "рыбалка", "karma", "карма", "fuck", "weather", "погода", "w", "п", "погодка", "погадка",
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(cmds, command) {
|
||||||
|
bingo = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не нашлось команды в одно слово. Поищем команды с одним аргументом.
|
||||||
|
if !bingo {
|
||||||
|
cmds = []string{
|
||||||
|
"w", "п", "weather", "погода", "погодка", "погадка", "karma", "карма",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cmds {
|
||||||
|
cRe := fmt.Sprintf("^%s ", c)
|
||||||
|
r := regexp.MustCompile(cRe)
|
||||||
|
|
||||||
|
if r.MatchString(command) {
|
||||||
|
bingo = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Что-то странное пришло. Залоггируем и ничего делать не будем. Просто свалим.
|
||||||
|
if !bingo {
|
||||||
|
log.Infof(
|
||||||
|
"Strange command from %s %s: %s",
|
||||||
|
ConstructFullChatName(&cmd.Message.Chat),
|
||||||
|
ConstructFullUserName(cmd.Message.From),
|
||||||
|
cmd.Message.Text,
|
||||||
|
)
|
||||||
|
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для некоторых команд надо подсвечивать имя пользователя в ответе.
|
||||||
|
cmds = []string{
|
||||||
|
"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка",
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(cmds, command) {
|
||||||
|
smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From)
|
||||||
|
smsg.Misc.Msgformat = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если у нас супергруппа с тредами, надо проставить thread id.
|
||||||
|
if cmd.Message.Chat.Type == "supergroup" && cmd.Message.IsTopicMessage && cmd.Message.ThreadID != 0 {
|
||||||
|
smsg.ThreadID = strconv.Itoa(cmd.Message.ThreadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем сообщение в редиску.
|
||||||
|
data, err := json.Marshal(smsg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||||
|
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заталкиваем наш json в редиску.
|
||||||
|
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
|
||||||
|
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
66
internal/tg/globals.go
Normal file
66
internal/tg/globals.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
"github.com/carlescere/scheduler"
|
||||||
|
"github.com/cockroachdb/pebble"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Config - это у нас глобальная штука.
|
||||||
|
Config MyConfig
|
||||||
|
|
||||||
|
// forwardMax is used to break circular message forwarding we must set some sane default, it can be overridden via config.
|
||||||
|
forwardMax int64 = 5
|
||||||
|
|
||||||
|
// RedisClient объектик клиента-редиски.
|
||||||
|
RedisClient *redis.Client
|
||||||
|
|
||||||
|
// Subscriber is redis PubSub object.
|
||||||
|
Subscriber *redis.PubSub
|
||||||
|
|
||||||
|
// Ctx is main context.
|
||||||
|
Ctx = context.Background()
|
||||||
|
|
||||||
|
// Shutdown ставится в true, если мы получили сигнал на выключение.
|
||||||
|
Shutdown = false
|
||||||
|
|
||||||
|
// SigChan канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
|
||||||
|
SigChan = make(chan os.Signal, 1)
|
||||||
|
|
||||||
|
// settingsDB мапка с открытыми дескрипторами баз с настройками.
|
||||||
|
settingsDB = make(map[string]*pebble.DB)
|
||||||
|
|
||||||
|
// chatListDB объектик *pebble.DB с базой, в которой лежит список чатов.
|
||||||
|
chatListDB *pebble.DB
|
||||||
|
|
||||||
|
// chatListDBName имя базы данных со списком чатов.
|
||||||
|
chatListDBName = "chat_list_db"
|
||||||
|
|
||||||
|
// chatList слайс со списоком чатов.
|
||||||
|
chatList = []string{}
|
||||||
|
|
||||||
|
// chatGroupList слайс со списком групповых чатов.
|
||||||
|
chatGroupList = []string{}
|
||||||
|
|
||||||
|
// tg is var for telegram api.
|
||||||
|
tg echotron.API
|
||||||
|
|
||||||
|
// introduceGreet приветственные фразы для новых участников чата.
|
||||||
|
introduceGreet = [...]string{
|
||||||
|
"Дратути",
|
||||||
|
"Дарована",
|
||||||
|
"Доброе утро, день или вечер",
|
||||||
|
"Добро пожаловать в наше скромное коммунити",
|
||||||
|
"Наше вам с кисточкой тут, на канальчике",
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeriodicJobs contains pointer to slice with sheduled job objects.
|
||||||
|
PeriodicJobs = make([]*scheduler.Job, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
70
internal/tg/help.go
Normal file
70
internal/tg/help.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Help Выводит в чат сообщение с основными командами бота.
|
||||||
|
func Help(cmd *echotron.Update) (bool, error) {
|
||||||
|
help := "```\n"
|
||||||
|
|
||||||
|
help += fmt.Sprintf("%shelp | %sпомощь - список команд", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", Config.Csign, Config.Csign, Config.Csign)
|
||||||
|
help += Config.Csign + "buni - рандомный стрип hapi buni"
|
||||||
|
help += fmt.Sprintf("%sbunny | %srabbit | %sкролик - кролик", Config.Csign, Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%scat | %sкис - кошечка", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%scoin | %sмонетка - подбросить монетку - орёл или решка?", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sdig | %sкопать - заняться археологией", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sdrink | %sпраздник - какой сегодня праздник?", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sfish | %sрыба | %sрыбка - порыбачить", Config.Csign, Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sfishing | %sрыбалка - порыбачить", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sf | %sф - рандомная фраза из сборника цитат fortune_mod", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sfortune | %sфортунка - рандомная фраза из сборника цитат fortune_mod", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sfox | %sлис - лисичка", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sfriday | %sпятница - а не пятница ли сегодня?", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sfrog | %sлягушка - лягушка", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%shorse | %sлошадка - лошадка", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%slat | %sлат - сгенерить фразу из крылатых латинских выражений", Config.Csign, Config.Csign)
|
||||||
|
help += Config.Csign + "monkeyuser - рандомный стрип MonkeyUser"
|
||||||
|
help += fmt.Sprintf("%sowl | %sсова | %sсыч - сова", Config.Csign, Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sproverb | %sпословица - рандомная русская пословица", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sping | %sпинг - попинговать бота", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sroll | %sdice | %sкости - бросить кости", Config.Csign, Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%ssnail | %sулитка - улитка", Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sver | %sversion | %sверсия - что-то про версию ПО", Config.Csign, Config.Csign, Config.Csign)
|
||||||
|
help += fmt.Sprintf("%sw <город> | %sп <город> - погода в указанном городе", Config.Csign, Config.Csign)
|
||||||
|
help += Config.Csign + "weather <город> - погода в указанном городе"
|
||||||
|
help += Config.Csign + "погода <город> - погода в указанном городе"
|
||||||
|
help += Config.Csign + "погодка <город> - погода в указанном городе"
|
||||||
|
help += Config.Csign + "погадка <город> - погода в указанном городе"
|
||||||
|
help += Config.Csign + "xkcd - рандомный стрип с сайта xkcd.ru"
|
||||||
|
help += fmt.Sprintf("%skarma фраза | %sкарма фраза - посмотреть карму фразы", Config.Csign, Config.Csign)
|
||||||
|
help += "```\n"
|
||||||
|
help += "Но на самом деле я бот больше для общения, чем для исполнения команд.\n"
|
||||||
|
help += "Поговоришь со мной?"
|
||||||
|
|
||||||
|
resp, err := tg.SendMessage(
|
||||||
|
help,
|
||||||
|
cmd.Message.Chat.ID,
|
||||||
|
&echotron.MessageOptions{ParseMode: "MarkdownV2"},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil || !resp.Ok {
|
||||||
|
// TODO: поддержать миграцию группы в супергруппу, поддержать вариант, когда бот замьючен.
|
||||||
|
// Красиво оформить ошибку, с полями итд, как tracedump, только ошибка.
|
||||||
|
// N.B. тут может быть сообщение о том, что группа превратилась в супергруппу, или что бот не имеет прав писать
|
||||||
|
// сообщения в чятик. Это надо бы отслеживать и хэндлить.
|
||||||
|
return true, fmt.Errorf(
|
||||||
|
"unable to send message to telegram api: %w\nResponse dump: %s",
|
||||||
|
err,
|
||||||
|
spew.Sdump(resp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
46
internal/tg/lib.go
Normal file
46
internal/tg/lib.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadConfig читает конфиг. Есть несколько преодопределённых локаций, откуда он пытается его вычитать.
|
||||||
|
// ./data/config.json, ~/.aleesa-telegram-go.json, ~/aleesa-telegram-go.json, ~/aleesa-telegram-go.json.
|
||||||
|
func ReadConfig() {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
executablePath, err := os.Executable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to get current executable path: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configJSONPath := filepath.Dir(executablePath) + "/data/config.json"
|
||||||
|
|
||||||
|
locations := []string{
|
||||||
|
"~/.aleesa-telegram-go.json",
|
||||||
|
"~/aleesa-telegram-go.json",
|
||||||
|
"/etc/aleesa-telegram-go.json",
|
||||||
|
configJSONPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, location := range locations {
|
||||||
|
Config, err = parseConfig(location)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf("Unable to parse config at %s: %s", location, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,61 +1,62 @@
|
|||||||
package main
|
package tg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
|
||||||
"github.com/hjson/hjson-go"
|
"github.com/hjson/hjson-go"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseConfig разбирает и валидирует даденный конфиг.
|
// parseConfig разбирает и валидирует даденный конфиг.
|
||||||
func parseConfig(path string) (myConfig, error) {
|
func parseConfig(path string) (MyConfig, error) {
|
||||||
fileInfo, err := os.Stat(path)
|
fileInfo, err := os.Stat(path)
|
||||||
|
|
||||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||||
if fileInfo.Size() > 65535 {
|
if fileInfo.Size() > 65535 {
|
||||||
err := fmt.Errorf("config file %s is too long for config, skipping", path)
|
err := fmt.Errorf("config file %s is too long for config, skipping", path)
|
||||||
|
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := os.ReadFile(path)
|
buf, err := os.ReadFile(path)
|
||||||
|
|
||||||
// Не удалось прочитать.
|
// Не удалось прочитать.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||||
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
|
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
|
||||||
var (
|
var (
|
||||||
sampleConfig myConfig
|
sampleConfig MyConfig
|
||||||
tmp map[string]interface{}
|
tmp map[string]any
|
||||||
)
|
)
|
||||||
|
|
||||||
err = hjson.Unmarshal(buf, &tmp)
|
err = hjson.Unmarshal(buf, &tmp)
|
||||||
|
|
||||||
// Не удалось распарсить.
|
// Не удалось распарсить.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpjson, err := json.Marshal(tmp)
|
tmpjson, err := json.Marshal(tmp)
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json.
|
// Не удалось преобразовать map-ку в json.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Валидируем значения из конфига.
|
// Валидируем значения из конфига.
|
||||||
@@ -72,29 +73,29 @@ func parseConfig(path string) (myConfig, error) {
|
|||||||
if sampleConfig.Redis.Channel == "" {
|
if sampleConfig.Redis.Channel == "" {
|
||||||
err := fmt.Errorf("channel field in config file %s must be set", path)
|
err := fmt.Errorf("channel field in config file %s must be set", path)
|
||||||
|
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Redis.MyChannel == "" {
|
if sampleConfig.Redis.MyChannel == "" {
|
||||||
err := fmt.Errorf("my_channel field in config file %s must be set", path)
|
err := fmt.Errorf("my_channel field in config file %s must be set", path)
|
||||||
|
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Telegram.Token == "" {
|
if sampleConfig.Telegram.Token == "" {
|
||||||
return myConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path)
|
return MyConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.Loglevel == "" {
|
if sampleConfig.Loglevel == "" {
|
||||||
sampleConfig.Loglevel = "info"
|
sampleConfig.Loglevel = "info"
|
||||||
}
|
}
|
||||||
|
|
||||||
// sampleConfig.Log = "" if not set
|
// sampleConfig.Log = "" if not set..
|
||||||
|
|
||||||
if sampleConfig.Csign == "" {
|
if sampleConfig.Csign == "" {
|
||||||
err := fmt.Errorf("csign field in config file %s must be set", path)
|
err := fmt.Errorf("csign field in config file %s must be set", path)
|
||||||
|
|
||||||
return myConfig{}, err
|
return MyConfig{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.ForwardsMax == 0 {
|
if sampleConfig.ForwardsMax == 0 {
|
||||||
@@ -102,7 +103,7 @@ func parseConfig(path string) (myConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sampleConfig.DataDir == "" {
|
if sampleConfig.DataDir == "" {
|
||||||
return myConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
|
return MyConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sampleConfig, err
|
return sampleConfig, err
|
||||||
83
internal/tg/periodicJobs.go
Normal file
83
internal/tg/periodicJobs.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// TidySettingsDB флашит на диск базу с настройками чатов. Рекомендуется делать это время от времени для поддержания
|
||||||
|
// производительности базы на должном уровне.
|
||||||
|
// Предполагается, что это происходит где-то раз в час, чаще смысла нет.
|
||||||
|
TidySettingsDB = func() {
|
||||||
|
if len(settingsDB) > 0 {
|
||||||
|
for name, db := range settingsDB {
|
||||||
|
log.Debugf("Flushing %s settings db", name)
|
||||||
|
|
||||||
|
if err := db.Flush(); err != nil {
|
||||||
|
log.Errorf("Unable to Flush() %s db: %s", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Flushing %s", chatListDBName)
|
||||||
|
|
||||||
|
if err := chatListDB.Flush(); err != nil {
|
||||||
|
log.Errorf("Unable to Flush() %s: %s", chatListDBName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendGoodMorning отсылает сообщение "с добрым утром" (сейчас - фортунку). Предполагается, что оно прогоняется по
|
||||||
|
// всем желающим чатам раз в сутки, с утра. Утро отсчитывается от настроек локали в системе.
|
||||||
|
SendGoodMorning = func() {
|
||||||
|
me, err := tg.GetMe()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to send good mornings message, cannot get info about myself: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chatID := range chatGroupList {
|
||||||
|
if GetSetting(chatID, "FortuneMsg") == "1" {
|
||||||
|
// Засылаем фразу в misc-канал (в роутер).
|
||||||
|
var rmsg rMsg
|
||||||
|
|
||||||
|
rmsg.Chatid = chatID
|
||||||
|
rmsg.Userid = strconv.FormatInt(me.Result.ID, 10)
|
||||||
|
rmsg.Message = "!f"
|
||||||
|
rmsg.Mode = "public"
|
||||||
|
rmsg.Plugin = "telegram"
|
||||||
|
rmsg.From = "telegram"
|
||||||
|
rmsg.Misc.Csign = Config.Csign
|
||||||
|
rmsg.Misc.Fwdcnt = 1
|
||||||
|
|
||||||
|
rmsg.Misc.Botnick = ConstructPartialUserUsername(me.Result)
|
||||||
|
rmsg.Misc.Username = ConstructPartialUserUsername(me.Result)
|
||||||
|
|
||||||
|
rmsg.Misc.GoodMorning = 1
|
||||||
|
// Форматирование нужно только для вывода некоторых ответов на команды, команды мы ловим выше по тексту, так что
|
||||||
|
// смело ставим тут 0.
|
||||||
|
rmsg.Misc.Msgformat = 0
|
||||||
|
|
||||||
|
data, err := json.Marshal(rmsg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заталкиваем наш json в редиску.
|
||||||
|
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
|
||||||
|
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
112
internal/tg/redisMsgParser.go
Normal file
112
internal/tg/redisMsgParser.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём json-чики должны быть относительно валидными.
|
||||||
|
func RedisMsgParser(msg string) {
|
||||||
|
if Shutdown {
|
||||||
|
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var j rMsg
|
||||||
|
|
||||||
|
log.Debugf("Incoming raw json: %s", msg)
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(msg), &j); err != nil {
|
||||||
|
log.Warnf("Unable to to parse message from redis channel: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate our j.
|
||||||
|
if exist := j.From; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no from field: %s", msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Chatid; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no chatid field: %s", msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Userid; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no userid field: %s", msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Message; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no message field: %s", msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Plugin; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no plugin field: %s", msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exist := j.Mode; exist == "" {
|
||||||
|
log.Warnf("Incorrect msg from redis, no mode field: %s", msg)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет.
|
||||||
|
if j.Misc.Answer == 0 {
|
||||||
|
log.Debug("Field Misc->Answer = 0, skipping message")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.BotNick тоже можно не передавать, тогда будет записана пустая строка
|
||||||
|
// j.Misc.CSign если нам его не передали, возьмём значение из конфига. (но по идее нам оно тут не нужнО.)
|
||||||
|
if exist := j.Misc.Csign; exist == "" {
|
||||||
|
j.Misc.Csign = Config.Csign
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.FwdCnt если нам его не передали, то будет 0.
|
||||||
|
if exist := j.Misc.Fwdcnt; exist == 0 {
|
||||||
|
j.Misc.Fwdcnt = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0.
|
||||||
|
// j.Misc.MsgFormat может быть 1 или 0, по-умолчанию 0.
|
||||||
|
// j.Misc.Username можно не передавать, тогда будет пустая строка.
|
||||||
|
|
||||||
|
// Отвалидировались, теперь вернёмся к нашим баранам.
|
||||||
|
|
||||||
|
var opts *echotron.MessageOptions
|
||||||
|
|
||||||
|
chatid, err := strconv.ParseInt(j.Chatid, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("unable to parse message from redis, incorrect chatid field: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := tg.SendMessage(j.Message, chatid, opts)
|
||||||
|
|
||||||
|
if err != nil || !resp.Ok {
|
||||||
|
// TODO: поддержать миграцию группы в супергруппу, поддержать вариант, когда бот замьючен.
|
||||||
|
// Красиво оформить ошибку, с полями итд, как tracedump, только ошибка.
|
||||||
|
// N.B. тут может быть сообщение о том, что группа превратилась в супергруппу, или что бот не имеет прав писать
|
||||||
|
// сообщения в чятик. Это надо бы отслеживать и хэндлить.
|
||||||
|
log.Errorf("Unable to send message to telegram api: %s", err)
|
||||||
|
log.Errorf("Response dump: %s", spew.Sdump(resp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
package main
|
package tg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
|
||||||
"github.com/cockroachdb/errors/oserror"
|
"github.com/cockroachdb/errors/oserror"
|
||||||
"github.com/cockroachdb/pebble"
|
"github.com/cockroachdb/pebble"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StoreKV сохраняет в указанной бд ключ и значение.
|
// StoreKV сохраняет в указанной бд ключ и значение.
|
||||||
@@ -44,6 +47,96 @@ func FetchV(db *pebble.DB, key string) (string, error) {
|
|||||||
return valueString, err
|
return valueString, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitChatListDB открывает для пользования базу со списком чатов.
|
||||||
|
// Вторым слоем этой базы являются слайсы chatList и chatGroupList, по которым можно в дальнейшем определять, известен
|
||||||
|
// ли даденный чат боту. По сути, слайс chatList активно пользуется в функции AppendChatListDB, а chatGroupList - в
|
||||||
|
// SendGoodMorning.
|
||||||
|
func InitChatListDB() error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
options pebble.Options
|
||||||
|
dbPath = Config.DataDir + "/" + chatListDBName
|
||||||
|
)
|
||||||
|
|
||||||
|
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||||
|
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||||
|
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||||
|
options.L0CompactionFileThreshold = 8
|
||||||
|
chatListDB, err = pebble.Open(dbPath, &options)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open settings db, %s: %w", dbPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заполним слайс со списком чатов. Операция нудная.
|
||||||
|
iterator, err := chatListDB.NewIter(&pebble.IterOptions{})
|
||||||
|
|
||||||
|
if !iterator.First() {
|
||||||
|
return errors.New("unable to iterate slice with chats")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попробуем извлечь список ключей и заполнить слайс с чатами.
|
||||||
|
for !iterator.Valid() {
|
||||||
|
// Битая база?
|
||||||
|
if err := iterator.Error(); err != nil {
|
||||||
|
chatList = []string{}
|
||||||
|
|
||||||
|
return fmt.Errorf("unable to get chat list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chatName := iterator.Key()
|
||||||
|
|
||||||
|
if len(chatName) == 0 {
|
||||||
|
return errors.New("got empty chat name when query " + chatListDBName)
|
||||||
|
}
|
||||||
|
|
||||||
|
chatID := string(chatName)
|
||||||
|
|
||||||
|
chatList = append(chatList, chatID)
|
||||||
|
slices.Sort(chatList)
|
||||||
|
|
||||||
|
if strings.HasPrefix(chatID, "-") {
|
||||||
|
chatGroupList = append(chatGroupList, chatID)
|
||||||
|
slices.Sort(chatGroupList)
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendChatListDB добавляет чат в список известных боту. Можно добавлять чаты не глядя, дубликатов не добавится.
|
||||||
|
// Bot API нам не даёт информацию о том, на что подписан бот.
|
||||||
|
func AppendChatListDB(chatID string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if slices.Contains(chatList, chatID) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = StoreKV(chatListDB, chatID, "1"); err != nil {
|
||||||
|
return fmt.Errorf("unable to append chat %s to chatListDB %s: %w", chatID, chatListDBName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Не забываем актуализировать список чатов.
|
||||||
|
chatList = append(chatList, chatID)
|
||||||
|
slices.Sort(chatList)
|
||||||
|
|
||||||
|
chatList = slices.Compact(chatList)
|
||||||
|
|
||||||
|
// Если СhatID начинается с - это значит, что мы имеем дело именно с группой/супергруппой/каналом.
|
||||||
|
if strings.HasPrefix(chatID, "-") {
|
||||||
|
chatGroupList = append(chatGroupList, chatID)
|
||||||
|
|
||||||
|
slices.Sort(chatGroupList)
|
||||||
|
|
||||||
|
chatGroupList = slices.Compact(chatGroupList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// GetSetting достаёт настройку из БД с настройками.
|
// GetSetting достаёт настройку из БД с настройками.
|
||||||
func GetSetting(chatID string, setting string) string {
|
func GetSetting(chatID string, setting string) string {
|
||||||
var err error
|
var err error
|
||||||
@@ -51,14 +144,14 @@ func GetSetting(chatID string, setting string) string {
|
|||||||
chatHash := sha256.Sum256([]byte(chatID))
|
chatHash := sha256.Sum256([]byte(chatID))
|
||||||
database := fmt.Sprintf("settings_db/%x", chatHash)
|
database := fmt.Sprintf("settings_db/%x", chatHash)
|
||||||
|
|
||||||
// Если БД не открыта, откроем её
|
// Если БД не открыта, откроем её.
|
||||||
if _, ok := settingsDB[database]; !ok {
|
if _, ok := settingsDB[database]; !ok {
|
||||||
var options pebble.Options
|
var options pebble.Options
|
||||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||||
options.L0CompactionFileThreshold = 8
|
options.L0CompactionFileThreshold = 8
|
||||||
settingsDB[database], err = pebble.Open(config.DataDir+"/"+database, &options)
|
settingsDB[database], err = pebble.Open(Config.DataDir+"/"+database, &options)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||||
@@ -104,7 +197,7 @@ func SaveSetting(chatID string, setting string, value string) error {
|
|||||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||||
options.L0CompactionFileThreshold = 8
|
options.L0CompactionFileThreshold = 8
|
||||||
|
|
||||||
settingsDB[database], err = pebble.Open(config.DataDir+"/"+database, &options)
|
settingsDB[database], err = pebble.Open(Config.DataDir+"/"+database, &options)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||||
@@ -113,8 +206,14 @@ func SaveSetting(chatID string, setting string, value string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := StoreKV(settingsDB[database], setting, value); err != nil {
|
if err = StoreKV(settingsDB[database], setting, value); err != nil {
|
||||||
log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
|
log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = settingsDB[database].Flush(); err != nil {
|
||||||
|
log.Errorf("Unable to flush settings db for %s: %s", chatID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
package main
|
package tg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
"aleesa-telegram-go/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
// SigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||||
func sigHandler() {
|
func SigHandler() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var s = <-sigChan
|
var s = <-SigChan
|
||||||
switch s {
|
switch s {
|
||||||
case syscall.SIGINT:
|
case syscall.SIGINT:
|
||||||
log.Infoln("Got SIGINT, quitting")
|
log.Info("Got SIGINT, quitting")
|
||||||
case syscall.SIGTERM:
|
case syscall.SIGTERM:
|
||||||
log.Infoln("Got SIGTERM, quitting")
|
log.Info("Got SIGTERM, quitting")
|
||||||
case syscall.SIGQUIT:
|
case syscall.SIGQUIT:
|
||||||
log.Infoln("Got SIGQUIT, quitting")
|
log.Info("Got SIGQUIT, quitting")
|
||||||
|
|
||||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||||
default:
|
default:
|
||||||
@@ -27,33 +27,27 @@ func sigHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true.
|
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true.
|
||||||
shutdown = true
|
Shutdown = true
|
||||||
|
|
||||||
// Отпишемся от всех каналов и закроем коннект к редиске.
|
// Отпишемся от всех каналов и закроем коннект к редиске.
|
||||||
if err = subscriber.Unsubscribe(ctx); err != nil {
|
if err = Subscriber.Unsubscribe(Ctx); err != nil {
|
||||||
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Unsubscribe from all redis channels")
|
log.Debug("Unsubscribe from all redis channels")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = subscriber.Close(); err != nil {
|
if err = Subscriber.Close(); err != nil {
|
||||||
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Close redis connection")
|
log.Debug("Close redis connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
// close all telego stuff.
|
// Quit all scheduled periodic jobs.
|
||||||
resp, err := tg.LogOut()
|
for _, job := range PeriodicJobs {
|
||||||
|
job.Quit <- true
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to send LogOut() to Telegram Bot API server")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !resp.Ok {
|
resp, err := tg.Close()
|
||||||
log.Errorf("Telegram Bot API returns an error on LogOut(): %d, %s", resp.ErrorCode, resp.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err = tg.Close()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to send Close() to Telegram Bot API server")
|
log.Errorf("Unable to send Close() to Telegram Bot API server")
|
||||||
@@ -71,6 +65,10 @@ func sigHandler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug("Closing known chat list db")
|
||||||
|
|
||||||
|
_ = chatListDB.Close()
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
package main
|
package tg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
|
||||||
"github.com/NicoNex/echotron/v3"
|
"github.com/NicoNex/echotron/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// telega основная горутинка, реализующая бота.
|
// Telega основная горутинка, реализующая бота.
|
||||||
func telega(c myConfig) {
|
func Telega(c MyConfig) {
|
||||||
tg = echotron.NewAPI(c.Telegram.Token)
|
tg = echotron.NewAPI(c.Telegram.Token)
|
||||||
|
|
||||||
for u := range echotron.PollingUpdates(c.Telegram.Token) {
|
for u := range echotron.PollingUpdates(c.Telegram.Token) {
|
||||||
@@ -73,10 +75,10 @@ func ConstructFullChatName(c *echotron.Chat) string {
|
|||||||
func ConstructPartialUserUsername(u *echotron.User) string {
|
func ConstructPartialUserUsername(u *echotron.User) string {
|
||||||
switch {
|
switch {
|
||||||
case u.Username != "":
|
case u.Username != "":
|
||||||
return fmt.Sprintf("@%s", u.Username)
|
return "@" + u.Username
|
||||||
|
|
||||||
case u.FirstName != "" && u.LastName != "":
|
case u.FirstName != "" && u.LastName != "":
|
||||||
return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
return u.FirstName + " " + u.LastName
|
||||||
|
|
||||||
case u.FirstName != "":
|
case u.FirstName != "":
|
||||||
return u.FirstName
|
return u.FirstName
|
||||||
@@ -85,19 +87,19 @@ func ConstructPartialUserUsername(u *echotron.User) string {
|
|||||||
return u.LastName
|
return u.LastName
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("%d", u.ID)
|
return strconv.FormatInt(u.ID, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
// ConstructPartialChatUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||||
// такового нет, то возвращает ID.
|
// такового нет, то возвращает ID.
|
||||||
func ConstructPartialChatUsername(c *echotron.Chat) string {
|
func ConstructPartialChatUsername(c *echotron.Chat) string {
|
||||||
switch {
|
switch {
|
||||||
case c.Username != "":
|
case c.Username != "":
|
||||||
return fmt.Sprintf("@%s", c.Username)
|
return "@" + c.Username
|
||||||
|
|
||||||
case c.FirstName != "" && c.LastName != "":
|
case c.FirstName != "" && c.LastName != "":
|
||||||
return fmt.Sprintf("%s %s", c.FirstName, c.LastName)
|
return c.FirstName + " " + c.LastName
|
||||||
|
|
||||||
case c.FirstName != "":
|
case c.FirstName != "":
|
||||||
return c.FirstName
|
return c.FirstName
|
||||||
@@ -106,7 +108,7 @@ func ConstructPartialChatUsername(c *echotron.Chat) string {
|
|||||||
return c.LastName
|
return c.LastName
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("%d", c.ID)
|
return strconv.FormatInt(c.ID, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,15 +119,15 @@ func ConstructUserFirstLastName(u *echotron.User) string {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case u.FirstName != "" && u.LastName != "":
|
case u.FirstName != "" && u.LastName != "":
|
||||||
user = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
user = u.FirstName + " " + u.LastName
|
||||||
case u.FirstName != "":
|
case u.FirstName != "":
|
||||||
user = u.FirstName
|
user = u.FirstName
|
||||||
case u.LastName != "":
|
case u.LastName != "":
|
||||||
user = u.LastName
|
user = u.LastName
|
||||||
case u.Username != "":
|
case u.Username != "":
|
||||||
user = fmt.Sprintf("@%s", u.Username)
|
user = "@" + u.Username
|
||||||
default:
|
default:
|
||||||
user = fmt.Sprintf("%d", u.ID)
|
user = strconv.FormatInt(u.ID, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
363
internal/tg/telegramMsgParser.go
Normal file
363
internal/tg/telegramMsgParser.go
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand/v2"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"aleesa-telegram-go/internal/log"
|
||||||
|
|
||||||
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
)
|
||||||
|
|
||||||
|
// telegramMsgParser парсит ивент, прилетевший из bot api.
|
||||||
|
func telegramMsgParser(msg *echotron.Update) {
|
||||||
|
if Shutdown {
|
||||||
|
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := AppendChatListDB(strconv.FormatInt(msg.ChatID(), 10)); err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сообщение о том, что этот чятик изменился, например, превратился в супергруппу.
|
||||||
|
if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) {
|
||||||
|
// TODO: поддержать миграцию настроек чата на новый chatId.
|
||||||
|
// TODO: подумать, что можно сделать с настройками бэкэндов. Кажись, ничего, но надо глянуть.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Здесь начинается парсинг, подразумевающий какие-то сообщения бота в чат, поэтому с этой точки можно/нужно
|
||||||
|
// получить информацию о самом боте.
|
||||||
|
// TODO: пораскинуть, можно ли как-то кэшировать эту информацию, чтобы не дёргать api по чём зря.
|
||||||
|
var (
|
||||||
|
rmsg rMsg
|
||||||
|
errorOccured bool
|
||||||
|
)
|
||||||
|
|
||||||
|
me, err := tg.GetMe()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to get info about myself, %s", err)
|
||||||
|
|
||||||
|
errorOccured = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Типа, при некоторых обстоятельствах, мы можем получить более внятное сообщение. Но это надо проверять.
|
||||||
|
if !me.Ok {
|
||||||
|
log.Errorf("Unable to get info about myself: %d %s", me.ErrorCode, me.Description)
|
||||||
|
|
||||||
|
errorOccured = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorOccured {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Люди пришли в чят.
|
||||||
|
if msg.Message.NewChatMembers != nil {
|
||||||
|
// TODO: проверить, надо ли приветствовать, выбрать одну из приветственных фраз, потянуть время, типа, мы пишем
|
||||||
|
// текст и поприветствовать вновь прибывшего.
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg") == "1" {
|
||||||
|
// Пользователи, которых мы приветсвуем, одной строкой.
|
||||||
|
var users string
|
||||||
|
|
||||||
|
if len(msg.Message.NewChatMembers) > 1 {
|
||||||
|
lastOne := msg.Message.NewChatMembers[len(msg.Message.NewChatMembers)-1]
|
||||||
|
theRest := msg.Message.NewChatMembers[:len(msg.Message.NewChatMembers)-1]
|
||||||
|
|
||||||
|
var usersSlice []string
|
||||||
|
|
||||||
|
for _, user := range theRest {
|
||||||
|
usersSlice = append(usersSlice, ConstructTelegramHighlightName(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
users = strings.Join(usersSlice, ", ")
|
||||||
|
|
||||||
|
users += " and " + ConstructTelegramHighlightName(lastOne)
|
||||||
|
} else {
|
||||||
|
users = ConstructTelegramHighlightName(msg.Message.NewChatMembers[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
phrase := fmt.Sprintf(
|
||||||
|
"%s, %s. Представьтес, пожалуйста, и расскажите, что вас сюда привело.",
|
||||||
|
introduceGreet[rand.IntN(len(introduceGreet))],
|
||||||
|
users,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Потянуть время, изобразить настоящего человека.
|
||||||
|
resp, err := tg.SendChatAction(
|
||||||
|
echotron.Typing,
|
||||||
|
msg.Message.From.ID,
|
||||||
|
&echotron.ChatActionOptions{MessageThreadID: msg.Message.ThreadID},
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или
|
||||||
|
// что-то, что тоже надо хэндлить.
|
||||||
|
if err != nil || !resp.Ok {
|
||||||
|
log.Errorf(
|
||||||
|
"unable to send message to telegram api: %s\nResponse dump: %s",
|
||||||
|
err,
|
||||||
|
spew.Sdump(resp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp1, err := tg.SendMessage(
|
||||||
|
phrase,
|
||||||
|
msg.Message.Chat.ID,
|
||||||
|
&echotron.MessageOptions{ParseMode: "MarkdownV2", MessageThreadID: int64(msg.Message.ThreadID)},
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или
|
||||||
|
// что-то, что тоже надо хэндлить.
|
||||||
|
if err != nil || !resp1.Ok {
|
||||||
|
log.Errorf(
|
||||||
|
"unable to send message to telegram api: %s\nResponse dump: %s",
|
||||||
|
err,
|
||||||
|
spew.Sdump(resp1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Человек ушёл из чата.
|
||||||
|
if msg.Message.LeftChatMember != nil {
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg") == "1" {
|
||||||
|
user := ConstructUserFirstLastName(msg.Message.From)
|
||||||
|
|
||||||
|
goodbye := "Прощаемся с " + user
|
||||||
|
|
||||||
|
opts := echotron.MessageOptions{DisableNotification: true, ParseMode: "MarkdownV2"}
|
||||||
|
|
||||||
|
if msg.Message.ThreadID != 0 {
|
||||||
|
opts.MessageThreadID = int64(msg.Message.ThreadID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := tg.SendMessage(goodbye, msg.Message.Chat.ID, &opts); err != nil {
|
||||||
|
log.Errorf("Unable to send message to telegram api: %s", err)
|
||||||
|
} else if !res.Ok {
|
||||||
|
log.Errorf("Unable to send message to telegram api: %s", res.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если сообщение было зацензурено, то дальше его обрабатывать не надо.
|
||||||
|
if Censor(msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Message.Chat.Type {
|
||||||
|
case "private":
|
||||||
|
// Всё что можно было похэндлить, что не содержало текста, считаем, что обработали.
|
||||||
|
if msg.Message.Text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считаем, что приватное сообщение всегда нуждается в ответе, иначе, зачем писать боту?
|
||||||
|
rmsg.Misc.Answer = 1
|
||||||
|
|
||||||
|
// Засылаем фразу в misc-канал (в роутер).
|
||||||
|
rmsg.Chatid = strconv.FormatInt(msg.Message.Chat.ID, 10)
|
||||||
|
rmsg.Userid = strconv.FormatInt(msg.Message.From.ID, 10)
|
||||||
|
rmsg.Message = msg.Message.Text
|
||||||
|
rmsg.Mode = "private"
|
||||||
|
rmsg.Plugin = "telegram"
|
||||||
|
rmsg.From = "telegram"
|
||||||
|
rmsg.Misc.Csign = Config.Csign
|
||||||
|
rmsg.Misc.Fwdcnt = 1
|
||||||
|
rmsg.Misc.Botnick = ConstructPartialUserUsername(me.Result)
|
||||||
|
rmsg.Misc.Username = ConstructPartialUserUsername(msg.Message.From)
|
||||||
|
rmsg.Misc.GoodMorning = 0
|
||||||
|
rmsg.Misc.Msgformat = 0
|
||||||
|
|
||||||
|
// Форматировать что-то всё-таки надо, но благо, это только команды, поэтому попробуем распознать сообщение как
|
||||||
|
// команду.
|
||||||
|
r, err := cmdParser(me, msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Infof(
|
||||||
|
"Unable to parse message as command in private conversation with %s, message was: %s",
|
||||||
|
ConstructFullUserName(msg.Message.From),
|
||||||
|
msg.Message.Text,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если сообщение не было опознанно как команда, засылаем его "как есть" туда, в качель.
|
||||||
|
if !r {
|
||||||
|
data, err := json.Marshal(rmsg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заталкиваем наш json в редиску.
|
||||||
|
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
|
||||||
|
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "group", "supergroup":
|
||||||
|
// Обрабатываем сообщения для групп и супергрупп. По сути это примерно одинаковые вещи/сущности.
|
||||||
|
// Цензор удаляет сообщения из чятика, если они "неправильные" - от имени других каналов, аудиосообщения,
|
||||||
|
// содержат картинки, видео аудио итп, это настраивается через команду !admin censor.
|
||||||
|
if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "censor") == "1" {
|
||||||
|
// Если цензор отработал, больше делать нечего, сообщения уже нету.
|
||||||
|
if Censor(msg) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если фраза менее 3-х букв, просто игнорируем её.
|
||||||
|
if len(msg.Message.Text) <= 3 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: если человек ответил на авто-приветствие, это надо обнаруживать и пропускать.
|
||||||
|
|
||||||
|
// Pасылаем сообщение в парсер команд.
|
||||||
|
res, err := cmdParser(me, msg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(
|
||||||
|
"Unable to parse message from telegram api as a command. Message was: %s, error: %s",
|
||||||
|
msg.Message.Text,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если сообщение - команда, то на этом наши приключения закончились.
|
||||||
|
if res {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить.
|
||||||
|
quotedBotUsername := regexp.QuoteMeta(me.Result.Username)
|
||||||
|
quotedBotFirstName := regexp.QuoteMeta(me.Result.FirstName)
|
||||||
|
quotedBotLastName := regexp.QuoteMeta(me.Result.LastName)
|
||||||
|
|
||||||
|
if messageContainsBotUsername, err := regexp.MatchString(quotedBotUsername, msg.Message.Text); err != nil {
|
||||||
|
log.Errorf("An error occured while matching message with bot username: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
} else if messageContainsBotUsername {
|
||||||
|
rmsg.Misc.Answer = 1
|
||||||
|
|
||||||
|
m := regexp.MustCompile(quotedBotUsername)
|
||||||
|
|
||||||
|
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: обращение может содержать знаки препинания, пробелы перед, пробелы после имени бота. Надо бы это поддержать.
|
||||||
|
// TODO: унести вырезание имени бота в отдельную процедуру.
|
||||||
|
switch {
|
||||||
|
// Если у нас есть и имя и фамилия бота, то попробуем их слепить вместе, через "1 и более" пробел и вырезать
|
||||||
|
// полученное из входящей фразы.
|
||||||
|
case quotedBotFirstName != "" && quotedBotLastName != "":
|
||||||
|
messageContainsBotName, err := regexp.MatchString(
|
||||||
|
quotedBotFirstName+"[[:space:]]+"+quotedBotLastName,
|
||||||
|
msg.Message.Text,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("An error occured while matching message with bot firstname and lastname: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageContainsBotName {
|
||||||
|
rmsg.Misc.Answer = 1
|
||||||
|
|
||||||
|
m := regexp.MustCompile(quotedBotFirstName + "[[:space:]]+" + quotedBotLastName)
|
||||||
|
|
||||||
|
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||||
|
}
|
||||||
|
// Если у нас есть только имя, то попробуем его вырезать из входящей фразы.
|
||||||
|
case quotedBotFirstName != "":
|
||||||
|
messageContainsBotName, err := regexp.MatchString(quotedBotFirstName, msg.Message.Text)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("An error occured while matching message with bot firstname: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageContainsBotName {
|
||||||
|
rmsg.Misc.Answer = 1
|
||||||
|
|
||||||
|
m := regexp.MustCompile(quotedBotFirstName)
|
||||||
|
|
||||||
|
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||||
|
}
|
||||||
|
// Если у нас есть только фамилия, то попробуем его вырезать из входящей фразы.
|
||||||
|
case quotedBotLastName != "":
|
||||||
|
messageContainsBotName, err := regexp.MatchString(quotedBotLastName, msg.Message.Text)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("An error occured while matching message with bot lastname: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if messageContainsBotName {
|
||||||
|
rmsg.Misc.Answer = 1
|
||||||
|
|
||||||
|
m := regexp.MustCompile(quotedBotLastName)
|
||||||
|
|
||||||
|
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Засылаем фразу в misc-канал (в роутер).
|
||||||
|
rmsg.Chatid = strconv.FormatInt(msg.Message.Chat.ID, 10)
|
||||||
|
rmsg.Userid = strconv.FormatInt(msg.Message.From.ID, 10)
|
||||||
|
rmsg.Message = msg.Message.Text
|
||||||
|
rmsg.Mode = "public"
|
||||||
|
rmsg.Plugin = "telegram"
|
||||||
|
rmsg.From = "telegram"
|
||||||
|
rmsg.Misc.Csign = Config.Csign
|
||||||
|
rmsg.Misc.Fwdcnt = 1
|
||||||
|
|
||||||
|
rmsg.Misc.Botnick = ConstructPartialUserUsername(me.Result)
|
||||||
|
rmsg.Misc.Username = ConstructPartialUserUsername(msg.Message.From)
|
||||||
|
|
||||||
|
rmsg.Misc.GoodMorning = 0
|
||||||
|
// Форматирование нужно только для вывода некоторых ответов на команды, команды мы ловим выше по тексту, так что
|
||||||
|
// смело ставим тут 0.
|
||||||
|
rmsg.Misc.Msgformat = 0
|
||||||
|
|
||||||
|
data, err := json.Marshal(rmsg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to to serialize message for redis: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заталкиваем наш json в редиску.
|
||||||
|
if err := RedisClient.Publish(Ctx, Config.Redis.Channel, data).Err(); err != nil {
|
||||||
|
log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// case "channel":
|
||||||
|
// Тут мы ничего сделать не можем.
|
||||||
|
} //nolint:wsl
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
71
internal/tg/types.go
Normal file
71
internal/tg/types.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package tg
|
||||||
|
|
||||||
|
type (
|
||||||
|
// MyConfig структура, описывающая формат Конфига.
|
||||||
|
MyConfig struct {
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
DataDir string `json:"data_dir,omitempty"`
|
||||||
|
Log string `json:"log,omitempty"`
|
||||||
|
Loglevel string `json:"loglevel,omitempty"`
|
||||||
|
|
||||||
|
Telegram struct {
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
} `json:"telegram"`
|
||||||
|
|
||||||
|
Redis struct {
|
||||||
|
Channel string `json:"channel,omitempty"`
|
||||||
|
MyChannel string `json:"my_channel,omitempty"`
|
||||||
|
Server string `json:"server,omitempty"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
} `json:"redis"`
|
||||||
|
|
||||||
|
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки.
|
||||||
|
rMsg struct {
|
||||||
|
From string `json:"from,omitempty"`
|
||||||
|
Chatid string `json:"chatid,omitempty"`
|
||||||
|
Userid string `json:"userid,omitempty"`
|
||||||
|
ThreadID string `json:"threadid,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Plugin string `json:"plugin,omitempty"`
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
|
Misc struct {
|
||||||
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
|
Csign string `json:"csign,omitempty"`
|
||||||
|
// Судя по перловой реализации, сюда всегда попадает [$fullname](tg://user?id=$userid).
|
||||||
|
// Согласно докуметации, и перловой реализации бота, мы проставляем это поле тогда, когда мы точно знаем, что в
|
||||||
|
// бэкэнде оно используется.
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Answer int64 `json:"answer,omitempty"`
|
||||||
|
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
||||||
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
|
// В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ
|
||||||
|
// не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда.
|
||||||
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
|
} `json:"Misc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки.
|
||||||
|
sMsg struct {
|
||||||
|
From string `json:"from"`
|
||||||
|
Chatid string `json:"chatid"`
|
||||||
|
Userid string `json:"userid"`
|
||||||
|
ThreadID string `json:"threadid,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Misc struct {
|
||||||
|
Botnick string `json:"bot_nick"`
|
||||||
|
Csign string `json:"csign"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Answer int64 `json:"answer"`
|
||||||
|
Fwdcnt int64 `json:"fwd_cnt"`
|
||||||
|
GoodMorning int64 `json:"good_morning"`
|
||||||
|
Msgformat int64 `json:"msg_format"`
|
||||||
|
} `json:"misc"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
117
main.go
117
main.go
@@ -1,117 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// init производит некоторую инициализацию перед запуском main().
|
|
||||||
func init() {
|
|
||||||
log.SetFormatter(&log.TextFormatter{
|
|
||||||
DisableQuote: true,
|
|
||||||
DisableLevelTruncation: false,
|
|
||||||
DisableColors: true,
|
|
||||||
FullTimestamp: true,
|
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
|
||||||
})
|
|
||||||
|
|
||||||
// no panic, no trace
|
|
||||||
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)
|
|
||||||
default:
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
executablePath, err := os.Executable()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Unable to get current executable path: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath))
|
|
||||||
|
|
||||||
locations := []string{
|
|
||||||
"~/.aleesa-telegram-go.json",
|
|
||||||
"~/aleesa-telegram-go.json",
|
|
||||||
"/etc/aleesa-telegram-go.json",
|
|
||||||
configJSONPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, location := range locations {
|
|
||||||
config, err = parseConfig(location)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Errorf("Unable to parse config at %s: %s", location, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Main context
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
// Откроем лог и скормим его логгеру.
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализируем redis-клиента.
|
|
||||||
redisClient = redis.NewClient(&redis.Options{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", config.Redis.Server, config.Redis.Port),
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Debugf("Lazy connect() to redis at %s:%d", config.Redis.Server, config.Redis.Port)
|
|
||||||
subscriber = redisClient.Subscribe(ctx, config.Redis.MyChannel)
|
|
||||||
redisMsgChan := subscriber.Channel()
|
|
||||||
|
|
||||||
// Самое время поставить траппер сигналов.
|
|
||||||
signal.Notify(sigChan,
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
syscall.SIGQUIT)
|
|
||||||
|
|
||||||
go sigHandler()
|
|
||||||
go telega(config)
|
|
||||||
|
|
||||||
// Обработчик событий от редиски.
|
|
||||||
for msg := range redisMsgChan {
|
|
||||||
if shutdown {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
redisMsgParser(msg.Payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
1188
msg-parsers.go
1188
msg-parsers.go
File diff suppressed because it is too large
Load Diff
68
types.go
68
types.go
@@ -1,68 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// myConfig структура, описывающая формат Конфига.
|
|
||||||
type myConfig struct {
|
|
||||||
Redis struct {
|
|
||||||
Server string `json:"server,omitempty"`
|
|
||||||
Port int `json:"port,omitempty"`
|
|
||||||
Channel string `json:"channel,omitempty"`
|
|
||||||
MyChannel string `json:"my_channel,omitempty"`
|
|
||||||
} `json:"redis"`
|
|
||||||
|
|
||||||
Telegram struct {
|
|
||||||
Token string `json:"token,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
Loglevel string `json:"loglevel,omitempty"`
|
|
||||||
Log string `json:"log,omitempty"`
|
|
||||||
Csign string `json:"csign,omitempty"`
|
|
||||||
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
|
||||||
DataDir string `json:"data_dir,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки.
|
|
||||||
type rMsg struct {
|
|
||||||
From string `json:"from,omitempty"`
|
|
||||||
Chatid string `json:"chatid,omitempty"`
|
|
||||||
Userid string `json:"userid,omitempty"`
|
|
||||||
ThreadID string `json:"threadid,omitempty"`
|
|
||||||
Message string `json:"message,omitempty"`
|
|
||||||
Plugin string `json:"plugin,omitempty"`
|
|
||||||
Mode string `json:"mode,omitempty"`
|
|
||||||
Misc struct {
|
|
||||||
Answer int64 `json:"answer,omitempty"`
|
|
||||||
Botnick string `json:"bot_nick,omitempty"`
|
|
||||||
Csign string `json:"csign,omitempty"`
|
|
||||||
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
|
||||||
GoodMorning int64 `json:"good_morning,omitempty"`
|
|
||||||
// В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ
|
|
||||||
// не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда.
|
|
||||||
Msgformat int64 `json:"msg_format,omitempty"`
|
|
||||||
// Судя по перловой реализации, сюда всегда попадает [$fullname](tg://user?id=$userid).
|
|
||||||
// Согласно докуметации, и перловой реализации бота, мы проставляем это поле тогда, когда мы точно знаем, что в
|
|
||||||
// бэкэнде оно используется.
|
|
||||||
Username string `json:"username,omitempty"`
|
|
||||||
} `json:"Misc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки.
|
|
||||||
type sMsg struct {
|
|
||||||
From string `json:"from"`
|
|
||||||
Chatid string `json:"chatid"`
|
|
||||||
Userid string `json:"userid"`
|
|
||||||
ThreadID string `json:"threadid,omitempty"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Plugin string `json:"plugin"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
Misc struct {
|
|
||||||
Answer int64 `json:"answer"`
|
|
||||||
Botnick string `json:"bot_nick"`
|
|
||||||
Csign string `json:"csign"`
|
|
||||||
Fwdcnt int64 `json:"fwd_cnt"`
|
|
||||||
GoodMorning int64 `json:"good_morning"`
|
|
||||||
Msgformat int64 `json:"msg_format"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
} `json:"misc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
|
||||||
27
vendor/github.com/carlescere/scheduler/.gitignore
generated
vendored
Normal file
27
vendor/github.com/carlescere/scheduler/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Editor stuff
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
16
vendor/github.com/carlescere/scheduler/.travis.yml
generated
vendored
Normal file
16
vendor/github.com/carlescere/scheduler/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.2
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
- 1.6
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get github.com/axw/gocov/gocov
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
|
||||||
|
- go get github.com/stretchr/testify/assert
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN
|
||||||
|
- go test -race
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014 Simon Eskildsen
|
Copyright (c) 2015 Carles Cerezo Guzmán
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -9,13 +9,14 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|||||||
copies of the Software, and to permit persons to whom the Software is
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
furnished to do so, subject to the following conditions:
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
The above copyright notice and this permission notice shall be included in all
|
||||||
all copies or substantial portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
THE SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
58
vendor/github.com/carlescere/scheduler/README.md
generated
vendored
Normal file
58
vendor/github.com/carlescere/scheduler/README.md
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# scheduler
|
||||||
|
[](https://godoc.org/github.com/carlescere/scheduler)
|
||||||
|
[](https://travis-ci.org/carlescere/scheduler)
|
||||||
|
[](https://coveralls.io/r/carlescere/scheduler?branch=master)
|
||||||
|
|
||||||
|
Job scheduling made easy.
|
||||||
|
|
||||||
|
Scheduler allows you to schedule recurrent jobs with an easy-to-read syntax.
|
||||||
|
|
||||||
|
Inspired by the article **[Rethinking Cron](http://adam.heroku.com/past/2010/4/13/rethinking_cron/)** and the **[schedule](https://github.com/dbader/schedule)** python module.
|
||||||
|
|
||||||
|
## How to use?
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/carlescere/scheduler"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
job := func() {
|
||||||
|
t := time.Now()
|
||||||
|
fmt.Println("Time's up! @", t.UTC())
|
||||||
|
}
|
||||||
|
// Run every 2 seconds but not now.
|
||||||
|
scheduler.Every(2).Seconds().NotImmediately().Run(job)
|
||||||
|
|
||||||
|
// Run now and every X.
|
||||||
|
scheduler.Every(5).Minutes().Run(job)
|
||||||
|
scheduler.Every().Day().Run(job)
|
||||||
|
scheduler.Every().Monday().At("08:30").Run(job)
|
||||||
|
|
||||||
|
// Keep the program from not exiting.
|
||||||
|
runtime.Goexit()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works?
|
||||||
|
By specifying the chain of calls, a `Job` struct is instantiated and a goroutine is starts observing the `Job`.
|
||||||
|
|
||||||
|
The goroutine will be on pause until:
|
||||||
|
* The next run scheduled is due. This will cause to execute the job.
|
||||||
|
* The `SkipWait` channel is activated. This will cause to execute the job.
|
||||||
|
* The `Quit` channel is activated. This will cause to finish the job.
|
||||||
|
|
||||||
|
## Not immediate recurrent jobs
|
||||||
|
By default the behaviour of the recurrent jobs (Every(N) seconds, minutes, hours) is to start executing the job right away and then wait the required amount of time. By calling specifically `.NotImmediately()` you can override that behaviour and not execute it directly when the function `Run()` is called.
|
||||||
|
|
||||||
|
```go
|
||||||
|
scheduler.Every(5).Minutes().NotImmediately().Run(job)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
Distributed under MIT license. See `LICENSE` for more information.
|
||||||
327
vendor/github.com/carlescere/scheduler/scheduler.go
generated
vendored
Normal file
327
vendor/github.com/carlescere/scheduler/scheduler.go
generated
vendored
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
// Package scheduler is a cron replacement based on:
|
||||||
|
// http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/
|
||||||
|
// and
|
||||||
|
// https://github.com/dbader/schedule
|
||||||
|
//
|
||||||
|
// Uses include:
|
||||||
|
// func main() {
|
||||||
|
// job := func() {
|
||||||
|
// fmt.Println("Time's up!")
|
||||||
|
// }
|
||||||
|
// scheduler.Every(5).Seconds().Run(function)
|
||||||
|
// scheduler.Every().Day().Run(function)
|
||||||
|
// scheduler.Every().Sunday().At("08:30").Run(function)
|
||||||
|
// }
|
||||||
|
package scheduler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scheduled interface {
|
||||||
|
nextRun() (time.Duration, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job defines a running job and allows to stop a scheduled job or run it.
|
||||||
|
type Job struct {
|
||||||
|
fn func()
|
||||||
|
Quit chan bool
|
||||||
|
SkipWait chan bool
|
||||||
|
err error
|
||||||
|
schedule scheduled
|
||||||
|
isRunning bool
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type recurrent struct {
|
||||||
|
units int
|
||||||
|
period time.Duration
|
||||||
|
done bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *recurrent) nextRun() (time.Duration, error) {
|
||||||
|
if r.units == 0 || r.period == 0 {
|
||||||
|
return 0, errors.New("cannot set recurrent time with 0")
|
||||||
|
}
|
||||||
|
if !r.done {
|
||||||
|
r.done = true
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return time.Duration(r.units) * r.period, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type daily struct {
|
||||||
|
hour int
|
||||||
|
min int
|
||||||
|
sec int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *daily) setTime(h, m, s int) {
|
||||||
|
d.hour = h
|
||||||
|
d.min = m
|
||||||
|
d.sec = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d daily) nextRun() (time.Duration, error) {
|
||||||
|
now := time.Now()
|
||||||
|
year, month, day := now.Date()
|
||||||
|
date := time.Date(year, month, day, d.hour, d.min, d.sec, 0, time.Local)
|
||||||
|
if now.Before(date) {
|
||||||
|
return date.Sub(now), nil
|
||||||
|
}
|
||||||
|
date = time.Date(year, month, day+1, d.hour, d.min, d.sec, 0, time.Local)
|
||||||
|
return date.Sub(now), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type weekly struct {
|
||||||
|
day time.Weekday
|
||||||
|
d daily
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w weekly) nextRun() (time.Duration, error) {
|
||||||
|
now := time.Now()
|
||||||
|
year, month, day := now.Date()
|
||||||
|
numDays := w.day - now.Weekday()
|
||||||
|
if numDays == 0 {
|
||||||
|
numDays = 7
|
||||||
|
} else if numDays < 0 {
|
||||||
|
numDays += 7
|
||||||
|
}
|
||||||
|
date := time.Date(year, month, day+int(numDays), w.d.hour, w.d.min, w.d.sec, 0, time.Local)
|
||||||
|
return date.Sub(now), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every defines when to run a job. For a recurrent jobs (n seconds/minutes/hours) you
|
||||||
|
// should specify the unit and then call to the correspondent period method.
|
||||||
|
func Every(times ...int) *Job {
|
||||||
|
switch len(times) {
|
||||||
|
case 0:
|
||||||
|
return &Job{}
|
||||||
|
case 1:
|
||||||
|
r := new(recurrent)
|
||||||
|
r.units = times[0]
|
||||||
|
return &Job{schedule: r}
|
||||||
|
default:
|
||||||
|
// Yeah... I don't like it either. But go does not support default
|
||||||
|
// parameters nor method overloading. In an ideal world should
|
||||||
|
// return an error at compile time not at runtime. :/
|
||||||
|
return &Job{err: errors.New("too many arguments in Every")}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotImmediately allows recurrent jobs not to be executed immediatelly after
|
||||||
|
// definition. If a job is declared hourly won't start executing until the first hour
|
||||||
|
// passed.
|
||||||
|
func (j *Job) NotImmediately() *Job {
|
||||||
|
rj, ok := j.schedule.(*recurrent)
|
||||||
|
if !ok {
|
||||||
|
j.err = errors.New("bad function chaining")
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
rj.done = true
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
// At lets you define a specific time when the job would be run. Does not work with
|
||||||
|
// recurrent jobs.
|
||||||
|
// Time should be defined as a string separated by a colon. Could be used as "08:35:30",
|
||||||
|
// "08:35" or "8" for only the hours.
|
||||||
|
func (j *Job) At(hourTime string) *Job {
|
||||||
|
if j.err != nil {
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
hour, min, sec, err := parseTime(hourTime)
|
||||||
|
if err != nil {
|
||||||
|
j.err = err
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
d, ok := j.schedule.(daily)
|
||||||
|
if !ok {
|
||||||
|
w, ok := j.schedule.(weekly)
|
||||||
|
if !ok {
|
||||||
|
j.err = errors.New("bad function chaining")
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
w.d.setTime(hour, min, sec)
|
||||||
|
j.schedule = w
|
||||||
|
} else {
|
||||||
|
d.setTime(hour, min, sec)
|
||||||
|
j.schedule = d
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run sets the job to the schedule and returns the pointer to the job so it may be
|
||||||
|
// stopped or executed without waiting or an error.
|
||||||
|
func (j *Job) Run(f func()) (*Job, error) {
|
||||||
|
if j.err != nil {
|
||||||
|
return nil, j.err
|
||||||
|
}
|
||||||
|
var next time.Duration
|
||||||
|
var err error
|
||||||
|
j.Quit = make(chan bool, 1)
|
||||||
|
j.SkipWait = make(chan bool, 1)
|
||||||
|
j.fn = f
|
||||||
|
// Check for possible errors in scheduling
|
||||||
|
next, err = j.schedule.nextRun()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go func(j *Job) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-j.Quit:
|
||||||
|
return
|
||||||
|
case <-j.SkipWait:
|
||||||
|
go runJob(j)
|
||||||
|
case <-time.After(next):
|
||||||
|
go runJob(j)
|
||||||
|
}
|
||||||
|
next, _ = j.schedule.nextRun()
|
||||||
|
}
|
||||||
|
}(j)
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) setRunning(running bool) {
|
||||||
|
j.Lock()
|
||||||
|
defer j.Unlock()
|
||||||
|
|
||||||
|
j.isRunning = running
|
||||||
|
}
|
||||||
|
|
||||||
|
func runJob(job *Job) {
|
||||||
|
if job.IsRunning() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job.setRunning(true)
|
||||||
|
job.fn()
|
||||||
|
job.setRunning(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTime(str string) (hour, min, sec int, err error) {
|
||||||
|
chunks := strings.Split(str, ":")
|
||||||
|
var hourStr, minStr, secStr string
|
||||||
|
switch len(chunks) {
|
||||||
|
case 1:
|
||||||
|
hourStr = chunks[0]
|
||||||
|
minStr = "0"
|
||||||
|
secStr = "0"
|
||||||
|
case 2:
|
||||||
|
hourStr = chunks[0]
|
||||||
|
minStr = chunks[1]
|
||||||
|
secStr = "0"
|
||||||
|
case 3:
|
||||||
|
hourStr = chunks[0]
|
||||||
|
minStr = chunks[1]
|
||||||
|
secStr = chunks[2]
|
||||||
|
}
|
||||||
|
hour, err = strconv.Atoi(hourStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, errors.New("bad time")
|
||||||
|
}
|
||||||
|
min, err = strconv.Atoi(minStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, errors.New("bad time")
|
||||||
|
}
|
||||||
|
sec, err = strconv.Atoi(secStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, errors.New("bad time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hour > 23 || min > 59 || sec > 59 {
|
||||||
|
return 0, 0, 0, errors.New("bad time")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) dayOfWeek(d time.Weekday) *Job {
|
||||||
|
if j.schedule != nil {
|
||||||
|
j.err = errors.New("bad function chaining")
|
||||||
|
}
|
||||||
|
j.schedule = weekly{day: d}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monday sets the job to run every Monday.
|
||||||
|
func (j *Job) Monday() *Job {
|
||||||
|
return j.dayOfWeek(time.Monday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tuesday sets the job to run every Tuesday.
|
||||||
|
func (j *Job) Tuesday() *Job {
|
||||||
|
return j.dayOfWeek(time.Tuesday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wednesday sets the job to run every Wednesday.
|
||||||
|
func (j *Job) Wednesday() *Job {
|
||||||
|
return j.dayOfWeek(time.Wednesday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thursday sets the job to run every Thursday.
|
||||||
|
func (j *Job) Thursday() *Job {
|
||||||
|
return j.dayOfWeek(time.Thursday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Friday sets the job to run every Friday.
|
||||||
|
func (j *Job) Friday() *Job {
|
||||||
|
return j.dayOfWeek(time.Friday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saturday sets the job to run every Saturday.
|
||||||
|
func (j *Job) Saturday() *Job {
|
||||||
|
return j.dayOfWeek(time.Saturday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sunday sets the job to run every Sunday.
|
||||||
|
func (j *Job) Sunday() *Job {
|
||||||
|
return j.dayOfWeek(time.Sunday)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day sets the job to run every day.
|
||||||
|
func (j *Job) Day() *Job {
|
||||||
|
if j.schedule != nil {
|
||||||
|
j.err = errors.New("bad function chaining")
|
||||||
|
}
|
||||||
|
j.schedule = daily{}
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Job) timeOfDay(d time.Duration) *Job {
|
||||||
|
if j.err != nil {
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
r := j.schedule.(*recurrent)
|
||||||
|
r.period = d
|
||||||
|
j.schedule = r
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seconds sets the job to run every n Seconds where n was defined in the Every
|
||||||
|
// function.
|
||||||
|
func (j *Job) Seconds() *Job {
|
||||||
|
return j.timeOfDay(time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minutes sets the job to run every n Minutes where n was defined in the Every
|
||||||
|
// function.
|
||||||
|
func (j *Job) Minutes() *Job {
|
||||||
|
return j.timeOfDay(time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hours sets the job to run every n Hours where n was defined in the Every function.
|
||||||
|
func (j *Job) Hours() *Job {
|
||||||
|
return j.timeOfDay(time.Hour)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRunning returns if the job is currently running
|
||||||
|
func (j *Job) IsRunning() bool {
|
||||||
|
j.RLock()
|
||||||
|
defer j.RUnlock()
|
||||||
|
return j.isRunning
|
||||||
|
}
|
||||||
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)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@@ -7,6 +7,9 @@ github.com/NicoNex/echotron/v3
|
|||||||
# github.com/beorn7/perks v1.0.1
|
# github.com/beorn7/perks v1.0.1
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/beorn7/perks/quantile
|
github.com/beorn7/perks/quantile
|
||||||
|
# github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
|
||||||
|
## explicit
|
||||||
|
github.com/carlescere/scheduler
|
||||||
# github.com/cespare/xxhash/v2 v2.3.0
|
# github.com/cespare/xxhash/v2 v2.3.0
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/cespare/xxhash/v2
|
github.com/cespare/xxhash/v2
|
||||||
@@ -163,9 +166,6 @@ github.com/prometheus/procfs/internal/util
|
|||||||
# github.com/rogpeppe/go-internal v1.12.0
|
# github.com/rogpeppe/go-internal v1.12.0
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
github.com/rogpeppe/go-internal/fmtsort
|
github.com/rogpeppe/go-internal/fmtsort
|
||||||
# github.com/sirupsen/logrus v1.9.3
|
|
||||||
## explicit; go 1.13
|
|
||||||
github.com/sirupsen/logrus
|
|
||||||
# golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
# golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
## explicit; go 1.20
|
## explicit; go 1.20
|
||||||
golang.org/x/exp/constraints
|
golang.org/x/exp/constraints
|
||||||
|
|||||||
Reference in New Issue
Block a user