Compare commits
29 Commits
75e337e64e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
124009613e | ||
|
|
ddcc8b1c87 | ||
|
|
bb2425f404 | ||
|
|
af7ff57279 | ||
|
|
5471e09a24 | ||
|
|
b50c0612a6 | ||
|
|
1e54dcf9f1 | ||
|
|
61208f5247 | ||
|
|
fbd0196a48 | ||
|
|
c31547ce4a | ||
|
|
490677de40 | ||
|
|
d8a92b37fc | ||
|
|
c149b92c0c | ||
|
|
5a54572b91 | ||
|
|
f427dca430 | ||
|
|
2af4cdd706 | ||
|
|
0c22128c77 | ||
|
|
037b07a00d | ||
|
|
64da7d6ce7 | ||
|
|
2bf30fc346 | ||
|
|
abbf2ca145 | ||
|
|
312153ad34 | ||
|
|
0472e0f5a8 | ||
|
|
a2e34ee12d | ||
|
|
12eab9c5dc | ||
|
|
a6960d0e86 | ||
|
|
618110a7b2 | ||
|
|
4ed399feb3 | ||
|
|
e725c31ca1 |
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
|
||||
[go.mod]
|
||||
indent_style = space
|
||||
indent_size = 8
|
||||
tab_width = 8
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
.idea/**
|
||||
.vscode/**
|
||||
.vstags
|
||||
data/config.json
|
||||
data/*_db
|
||||
aleesa-telegram-go
|
||||
/data/config.json
|
||||
/data/*_db
|
||||
/data/cache
|
||||
/aleesa-telegram-go
|
||||
/*.log
|
||||
/*.bck.*
|
||||
|
||||
743
.golangci.yml
743
.golangci.yml
@@ -1,28 +1,12 @@
|
||||
version: "2"
|
||||
run:
|
||||
tests: false
|
||||
skip-dirs:
|
||||
- data
|
||||
- vendor
|
||||
go: "1.25"
|
||||
modules-download-mode: readonly
|
||||
tests: false
|
||||
allow-parallel-runners: true
|
||||
go: '1.21'
|
||||
output:
|
||||
sort-results: true
|
||||
linters-settings:
|
||||
gosec:
|
||||
excludes:
|
||||
# Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки
|
||||
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
|
||||
- G404 # Insecure random number source (rand)
|
||||
linters:
|
||||
disable-all: true
|
||||
default: none
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
@@ -30,24 +14,30 @@ linters:
|
||||
- contextcheck
|
||||
- decorder
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- gci
|
||||
- exptostd
|
||||
- ginkgolinter
|
||||
- gocheckcompilerdirectives
|
||||
- gocritic
|
||||
- godoclint
|
||||
- godot
|
||||
- gofmt
|
||||
- goimports
|
||||
- godox
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- govet
|
||||
- grouper
|
||||
- importas
|
||||
- ineffassign
|
||||
- intrange
|
||||
- ireturn
|
||||
- makezero
|
||||
- mirror
|
||||
- misspell
|
||||
- modernize
|
||||
- nakedret
|
||||
- nilerr
|
||||
- nilnil
|
||||
@@ -55,13 +45,714 @@ linters:
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- perfsprint
|
||||
- predeclared
|
||||
- promlinter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- stylecheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
rm -rf vendor
|
||||
go get -d -u -t ./...
|
||||
go get -u ./...
|
||||
go mod tidy
|
||||
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:
|
||||
|
||||
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.
|
||||
"cachedir" : "data/cache",
|
||||
# Data_dir used not only for caches but for some settings and stats too.
|
||||
"data_dir" : "data",
|
||||
|
||||
# Required for debugging reasons, if commented out or not exist, no debug logging appears at all.
|
||||
"log" : "/var/log/aleesa-telegram/debug.log",
|
||||
|
||||
# Verbosity can be trace, debug, info (inform), notice, warning (warn), error (err), critical (crit, fatal), alert, emergency.
|
||||
# Currently implemented debug, info, notice, warn, error, fatal. Notice is default logging level.
|
||||
"loglevel": "notice",
|
||||
# Verbosity can be trace, debug, info, warn, error.
|
||||
# Currently implemented debug, info, warn, error. Info is default logging level.
|
||||
"loglevel": "info",
|
||||
|
||||
# Stuff for telegram itself.
|
||||
"telegrambot" : {
|
||||
"telegram" : {
|
||||
"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" : "!",
|
||||
|
||||
"token" : "Long_string_given_by_@BotFather"
|
||||
},
|
||||
|
||||
# Unprivileged user to run this bot from.
|
||||
"user" : "nobody",
|
||||
|
||||
"redis_server": "redis",
|
||||
"redis_port" : "6379",
|
||||
"redis_channels" : [
|
||||
"telegram"
|
||||
],
|
||||
|
||||
"redis_router_channel" : "misc"
|
||||
"redis": {
|
||||
"server": "redis",
|
||||
"port" : 6379,
|
||||
"channel" : "misc",
|
||||
"my_channel": "telegram"
|
||||
}
|
||||
}
|
||||
|
||||
// vim: set ft=json noet ai ts=4 sw=4 sts=4:
|
||||
|
||||
34
globals.go
34
globals.go
@@ -1,34 +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
|
||||
35
go.mod
35
go.mod
@@ -1,39 +1,42 @@
|
||||
module aleesa-telegram-go
|
||||
|
||||
go 1.19
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/NicoNex/echotron/v3 v3.29.0
|
||||
github.com/cockroachdb/errors v1.11.1
|
||||
github.com/cockroachdb/pebble v1.1.0
|
||||
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/pebble v1.1.1
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/hjson/hjson-go v3.3.0+incompatible
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/DataDog/zstd v1.5.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a // indirect
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
|
||||
github.com/cockroachdb/redact v1.1.5 // indirect
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/getsentry/sentry-go v0.27.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.28.1 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.18.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
||||
94
go.sum
94
go.sum
@@ -1,32 +1,38 @@
|
||||
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
|
||||
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/NicoNex/echotron/v3 v3.29.0 h1:STzeYNauF+h82vTL9QIqIBEvhke/bsxgYKQMCmJKnLM=
|
||||
github.com/NicoNex/echotron/v3 v3.29.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI=
|
||||
github.com/NicoNex/echotron/v3 v3.33.1 h1:P9bVvmnuVd0C7bR5q9TV6Pece/wBqZ6PSo0RqokZgng=
|
||||
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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
||||
github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8=
|
||||
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a h1:f52TdbU4D5nozMAhO9TvTJ2ZMCXtN4VIAmfrrZ0JXQ4=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240616162244-4768e80dfb9a/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4=
|
||||
github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E=
|
||||
github.com/cockroachdb/pebble v1.1.1 h1:XnKU22oiCLy2Xn8vp1re67cXg4SAasg/WDt1NtcRFaw=
|
||||
github.com/cockroachdb/pebble v1.1.1/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU=
|
||||
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
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/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/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/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
|
||||
github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -34,69 +40,76 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
|
||||
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
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/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.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
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-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -105,10 +118,11 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
|
||||
"github.com/hjson/hjson-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// parseConfig разбирает и валидирует даденный конфиг.
|
||||
func parseConfig(path string) (myConfig, error) {
|
||||
func parseConfig(path string) (MyConfig, error) {
|
||||
fileInfo, err := os.Stat(path)
|
||||
|
||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||
if fileInfo.Size() > 65535 {
|
||||
err := fmt.Errorf("config file %s is too long for config, skipping", path)
|
||||
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(path)
|
||||
|
||||
// Не удалось прочитать.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
|
||||
var (
|
||||
sampleConfig myConfig
|
||||
tmp map[string]interface{}
|
||||
sampleConfig MyConfig
|
||||
tmp map[string]any
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json.
|
||||
if err != nil {
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
err := fmt.Errorf("channel field in config file %s must be set", path)
|
||||
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.MyChannel == "" {
|
||||
err := fmt.Errorf("my_channel field in config file %s must be set", path)
|
||||
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
sampleConfig.Loglevel = "info"
|
||||
}
|
||||
|
||||
// sampleConfig.Log = "" if not set
|
||||
// sampleConfig.Log = "" if not set..
|
||||
|
||||
if sampleConfig.Csign == "" {
|
||||
err := fmt.Errorf("csign field in config file %s must be set", path)
|
||||
|
||||
return myConfig{}, err
|
||||
return MyConfig{}, err
|
||||
}
|
||||
|
||||
if sampleConfig.ForwardsMax == 0 {
|
||||
@@ -102,8 +103,10 @@ func parseConfig(path string) (myConfig, error) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
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: */
|
||||
222
internal/tg/settings-db-util.go
Normal file
222
internal/tg/settings-db-util.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
|
||||
"github.com/cockroachdb/errors/oserror"
|
||||
"github.com/cockroachdb/pebble"
|
||||
)
|
||||
|
||||
// StoreKV сохраняет в указанной бд ключ и значение.
|
||||
func StoreKV(db *pebble.DB, key string, value string) error {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
vArray = []byte(value)
|
||||
)
|
||||
|
||||
err := db.Set(kArray, vArray, pebble.Sync)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchV достаёт значение по ключу.
|
||||
func FetchV(db *pebble.DB, key string) (string, error) {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
|
||||
vArray []byte
|
||||
valueString = ""
|
||||
)
|
||||
|
||||
vArray, closer, err := db.Get(kArray)
|
||||
|
||||
if err != nil {
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
valueString = string(vArray)
|
||||
err = closer.Close()
|
||||
|
||||
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 достаёт настройку из БД с настройками.
|
||||
func GetSetting(chatID string, setting string) string {
|
||||
var err error
|
||||
|
||||
chatHash := sha256.Sum256([]byte(chatID))
|
||||
database := fmt.Sprintf("settings_db/%x", chatHash)
|
||||
|
||||
// Если БД не открыта, откроем её.
|
||||
if _, ok := settingsDB[database]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||
options.L0CompactionFileThreshold = 8
|
||||
settingsDB[database], err = pebble.Open(Config.DataDir+"/"+database, &options)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
value, err := FetchV(settingsDB[database], setting)
|
||||
|
||||
// Если из базы ничего не вынулось, по каким-то причинам, то просто вернём пустую строку.
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, pebble.ErrNotFound):
|
||||
log.Debugf("Unable to get value for %s: no record found in db %s", setting, database)
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
|
||||
case errors.Is(err, oserror.ErrNotExist):
|
||||
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
|
||||
default:
|
||||
log.Errorf("Unable to get value for %s in db dir %s: %s", setting, database, err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// SaveSetting сохраняет настройку в БД с настройками.
|
||||
func SaveSetting(chatID string, setting string, value string) error {
|
||||
var (
|
||||
chatHash = sha256.Sum256([]byte(chatID))
|
||||
database = fmt.Sprintf("settings_db/%x", chatHash)
|
||||
err error
|
||||
)
|
||||
|
||||
// Если БД не открыта, откроем её.
|
||||
if _, ok := settingsDB[database]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||
options.L0CompactionFileThreshold = 8
|
||||
|
||||
settingsDB[database], err = pebble.Open(Config.DataDir+"/"+database, &options)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = StoreKV(settingsDB[database], setting, value); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
76
internal/tg/sigHandler.go
Normal file
76
internal/tg/sigHandler.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
)
|
||||
|
||||
// SigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||
func SigHandler() {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var s = <-SigChan
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
log.Info("Got SIGINT, quitting")
|
||||
case syscall.SIGTERM:
|
||||
log.Info("Got SIGTERM, quitting")
|
||||
case syscall.SIGQUIT:
|
||||
log.Info("Got SIGQUIT, quitting")
|
||||
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true.
|
||||
Shutdown = true
|
||||
|
||||
// Отпишемся от всех каналов и закроем коннект к редиске.
|
||||
if err = Subscriber.Unsubscribe(Ctx); err != nil {
|
||||
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
||||
} else {
|
||||
log.Debug("Unsubscribe from all redis channels")
|
||||
}
|
||||
|
||||
if err = Subscriber.Close(); err != nil {
|
||||
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
||||
} else {
|
||||
log.Debug("Close redis connection")
|
||||
}
|
||||
|
||||
// Quit all scheduled periodic jobs.
|
||||
for _, job := range PeriodicJobs {
|
||||
job.Quit <- true
|
||||
}
|
||||
|
||||
resp, err := tg.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to send Close() to Telegram Bot API server")
|
||||
}
|
||||
|
||||
if !resp.Ok {
|
||||
log.Errorf("Telegram Bot API returns an error on Close(): %d, %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
|
||||
if len(settingsDB) > 0 {
|
||||
log.Debug("Closing runtime bot settings db")
|
||||
|
||||
for _, db := range settingsDB {
|
||||
_ = db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("Closing known chat list db")
|
||||
|
||||
_ = chatListDB.Close()
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
162
internal/tg/telegram.go
Normal file
162
internal/tg/telegram.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package tg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"aleesa-telegram-go/internal/log"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// Telega основная горутинка, реализующая бота.
|
||||
func Telega(c MyConfig) {
|
||||
tg = echotron.NewAPI(c.Telegram.Token)
|
||||
|
||||
for u := range echotron.PollingUpdates(c.Telegram.Token) {
|
||||
telegramMsgParser(u)
|
||||
}
|
||||
}
|
||||
|
||||
func delMsg(msg *echotron.Update) {
|
||||
if res, err := tg.DeleteMessage(msg.ChatID(), msg.Message.ID); err != nil {
|
||||
chat := ConstructFullChatName(msg.Message.SenderChat)
|
||||
user := ConstructFullUserName(msg.Message.From)
|
||||
|
||||
log.Errorf("Unable to delete message %d in chat %s from %s via telegram api: %s", msg.Message.ID, chat, user, err)
|
||||
} else if !res.Ok {
|
||||
chat := ConstructFullChatName(msg.Message.SenderChat)
|
||||
user := ConstructFullUserName(msg.Message.From)
|
||||
|
||||
log.Errorf("Unable to delete message %d in chat %s from %s via telegram api: %s", msg.ID, chat, user, res.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructFullUserName выковыривает из сообщения полный username, в формате @username FirstName LastName (id).
|
||||
func ConstructFullUserName(u *echotron.User) string {
|
||||
user := fmt.Sprintf("(%d)", u.ID)
|
||||
|
||||
if u.LastName != "" {
|
||||
user = fmt.Sprintf("%s %s", u.LastName, user)
|
||||
}
|
||||
|
||||
if u.FirstName != "" {
|
||||
user = fmt.Sprintf("%s %s", u.FirstName, user)
|
||||
}
|
||||
|
||||
if u.Username != "" {
|
||||
user = fmt.Sprintf("@%s %s", u.Username, user)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// ConstructFullChatName выковыривает из сообщения полный username чата, в формате @username FirstName LastName (id).
|
||||
func ConstructFullChatName(c *echotron.Chat) string {
|
||||
chat := fmt.Sprintf("(%d)", c.ID)
|
||||
|
||||
if c.LastName != "" {
|
||||
chat = fmt.Sprintf("%s %s", c.LastName, chat)
|
||||
}
|
||||
|
||||
if c.FirstName != "" {
|
||||
chat = fmt.Sprintf("%s %s", c.FirstName, chat)
|
||||
}
|
||||
|
||||
if c.Username != "" {
|
||||
chat = fmt.Sprintf("@%s %s", c.Username, chat)
|
||||
}
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||
// такового нет, то возвращает ID.
|
||||
func ConstructPartialUserUsername(u *echotron.User) string {
|
||||
switch {
|
||||
case u.Username != "":
|
||||
return "@" + u.Username
|
||||
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
return u.FirstName + " " + u.LastName
|
||||
|
||||
case u.FirstName != "":
|
||||
return u.FirstName
|
||||
|
||||
case u.LastName != "":
|
||||
return u.LastName
|
||||
|
||||
default:
|
||||
return strconv.FormatInt(u.ID, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructPartialChatUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||
// такового нет, то возвращает ID.
|
||||
func ConstructPartialChatUsername(c *echotron.Chat) string {
|
||||
switch {
|
||||
case c.Username != "":
|
||||
return "@" + c.Username
|
||||
|
||||
case c.FirstName != "" && c.LastName != "":
|
||||
return c.FirstName + " " + c.LastName
|
||||
|
||||
case c.FirstName != "":
|
||||
return c.FirstName
|
||||
|
||||
case c.LastName != "":
|
||||
return c.LastName
|
||||
|
||||
default:
|
||||
return strconv.FormatInt(c.ID, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructUserFirstLastName Пытается найти и вытащить first name и last name пользователя, если не получается, то
|
||||
// вначале пытается фоллбэчится на first name, потом на last name, потом на username.
|
||||
func ConstructUserFirstLastName(u *echotron.User) string {
|
||||
var user string
|
||||
|
||||
switch {
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
user = u.FirstName + " " + u.LastName
|
||||
case u.FirstName != "":
|
||||
user = u.FirstName
|
||||
case u.LastName != "":
|
||||
user = u.LastName
|
||||
case u.Username != "":
|
||||
user = "@" + u.Username
|
||||
default:
|
||||
user = strconv.FormatInt(u.ID, 10)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// ConstructTelegramHighlightName генерирует имя пользователя, которое триггерит на стороне клиента ивент меншена
|
||||
// определённого пользователя.
|
||||
func ConstructTelegramHighlightName(u *echotron.User) string {
|
||||
var (
|
||||
username string
|
||||
link string
|
||||
)
|
||||
|
||||
link = fmt.Sprintf("tg://user?id=%d", u.ID)
|
||||
|
||||
// Тут мы предполагаем, что как минимум либо firstname либо lastname либо username всегда есть. По сути так оно и
|
||||
// должно быть.
|
||||
switch {
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
username = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
||||
case u.FirstName != "":
|
||||
username = u.FirstName
|
||||
case u.LastName != "":
|
||||
username = u.LastName
|
||||
default:
|
||||
username = u.Username
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s](%s)", username, link)
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
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: */
|
||||
115
main.go
115
main.go
@@ -1,115 +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)
|
||||
}
|
||||
}
|
||||
641
msg-parsers.go
641
msg-parsers.go
@@ -1,641 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// telego msg parser
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
// telegramMsgParser парсит ивент, прилетевший из bot api.
|
||||
func telegramMsgParser(msg *echotron.Update) {
|
||||
if shutdown {
|
||||
// Если мы завершаем работу программы, то нам ничего обрабатывать не надо.
|
||||
return
|
||||
}
|
||||
|
||||
// Сообщение о том, что этот чятик изменился, например превратился в супергруппу.
|
||||
if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) {
|
||||
// TODO: поддержать миграцию настроек чата на новый chatId.
|
||||
// TODO: подумать, что можно сделать с настройками бэкэндов. Кажись, ничего, но надо глянуть.
|
||||
}
|
||||
|
||||
// Люди пришли в чят.
|
||||
if msg.Message.NewChatMembers != nil {
|
||||
// TODO: проверить, надо ли приветствовать, выбрать одну из приветственных фраз, потянуть время, типа, мы пишем
|
||||
// текст и поприветствовать вновь прибывшего.
|
||||
}
|
||||
|
||||
// Человек ушёл из чата.
|
||||
if msg.Message.LeftChatMember != nil {
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" {
|
||||
user := ConstructUserFirstLastName(msg.Message.From)
|
||||
|
||||
goodbye := fmt.Sprintf("Прощаемся с %s", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
switch msg.Message.Chat.Type {
|
||||
case "private":
|
||||
// Всё что можно было похэндлить, что не содержало текста, считаем, что обработали.
|
||||
if msg.Message.Text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Считаем, что приватное сообщение всегда нуждается в ответе, иначе, зачем писать боту?
|
||||
rmsg.Misc.Answer = 1
|
||||
|
||||
// Засылаем фразу в misc-канал (в роутер).
|
||||
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID)
|
||||
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID)
|
||||
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
|
||||
|
||||
// TODO: детектить нужно ли форматировать ответ. Для этого фактически надо парсить простые команды, как минимум.
|
||||
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 "group", "supergroup":
|
||||
// Обрабатываем сообщения для групп и супергрупп. По сути это примерно одинаковые вещи/сущности.
|
||||
|
||||
// Цензор удаляет сообщения из чятика, если они "неправильные" - от имени других каналов, аудиосообщения,
|
||||
// содержат картинки, видео аудио итп, это настраивается через команду !admin censor.
|
||||
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "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: обращение может содержать знаки препинания, пробелы перед, пробелы после. Надо бы это поддержать.
|
||||
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 = fmt.Sprintf("%d", msg.Message.Chat.ID)
|
||||
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID)
|
||||
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
|
||||
|
||||
// TODO: детектить нужно ли форматировать ответ. Для этого фактически надо парсить простые команды, как минимум.
|
||||
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
|
||||
}
|
||||
|
||||
// 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 = fmt.Sprintf("%d", cmd.Message.Chat.ID)
|
||||
smsg.Userid = fmt.Sprintf("%d", cmd.Message.From.ID)
|
||||
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):]
|
||||
|
||||
// TODO: поддержать команды ver version версия, help, admin и admin *.
|
||||
|
||||
// Команды в одно слово.
|
||||
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",
|
||||
}
|
||||
|
||||
for _, c := range cmds {
|
||||
if command == c {
|
||||
bingo = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Не нашлось команды в одно слово. Поищем команды с одним аргументом.
|
||||
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", "рыба", "рыбка", "рыбалка",
|
||||
}
|
||||
|
||||
for _, c := range cmds {
|
||||
// В vscode возникает ошибка при использовании if-а.
|
||||
switch {
|
||||
case command == c:
|
||||
smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From)
|
||||
smsg.Misc.Msgformat = 1
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Если у нас супергруппа с тредами, надо проставить thread id.
|
||||
if cmd.Message.Chat.Type == "supergroup" && cmd.Message.IsTopicMessage && cmd.Message.ThreadID != 0 {
|
||||
smsg.ThreadID = fmt.Sprintf("%d", 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
|
||||
}
|
||||
|
||||
// Censor парсит сообщения в поисках непотребных данных и если он их находит, то сообщение удаляется.
|
||||
// Непотребными могут быть аудиосообщения, аудиофайлы, видеосообщения, сообщения от имени других каналов итп.
|
||||
// Это могут настроить админы чятика через команду !admin censor.
|
||||
// TODO: Реализовать censor-а.
|
||||
func Censor(msg *echotron.Update) bool {
|
||||
result := false
|
||||
|
||||
if GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VoiceMsg") == "1" {
|
||||
result = true
|
||||
|
||||
if res, err := tg.DeleteMessage(msg.ChatID(), msg.Message.ID); err != nil {
|
||||
chat := ConstructFullChatName(msg.Message.SenderChat)
|
||||
user := ConstructFullUserName(msg.Message.From)
|
||||
|
||||
log.Errorf("Unable to delete message %d in chat %s from %s via telegram api: %s", msg.Message.ID, chat, user, err)
|
||||
} else if !res.Ok {
|
||||
chat := ConstructFullChatName(msg.Message.SenderChat)
|
||||
user := ConstructFullUserName(msg.Message.From)
|
||||
|
||||
log.Errorf("Unable to delete message %d in chat %s from %s via telegram api: %s", msg.ID, chat, user, res.Description)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ConstructFullUserName выковыривает из сообщения полный username, в формате @username FirstName LastName (id).
|
||||
func ConstructFullUserName(u *echotron.User) string {
|
||||
user := fmt.Sprintf("(%d)", u.ID)
|
||||
|
||||
if u.LastName != "" {
|
||||
user = fmt.Sprintf("%s %s", u.LastName, user)
|
||||
}
|
||||
|
||||
if u.FirstName != "" {
|
||||
user = fmt.Sprintf("%s %s", u.FirstName, user)
|
||||
}
|
||||
|
||||
if u.Username != "" {
|
||||
user = fmt.Sprintf("@%s %s", u.Username, user)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// ConstructFullChatName выковыривает из сообщения полный username чата, в формате @username FirstName LastName (id).
|
||||
func ConstructFullChatName(c *echotron.Chat) string {
|
||||
chat := fmt.Sprintf("(%d)", c.ID)
|
||||
|
||||
if c.LastName != "" {
|
||||
chat = fmt.Sprintf("%s %s", c.LastName, chat)
|
||||
}
|
||||
|
||||
if c.FirstName != "" {
|
||||
chat = fmt.Sprintf("%s %s", c.FirstName, chat)
|
||||
}
|
||||
|
||||
if c.Username != "" {
|
||||
chat = fmt.Sprintf("@%s %s", c.Username, chat)
|
||||
}
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||
// такового нет, то возвращает ID.
|
||||
func ConstructPartialUserUsername(u *echotron.User) string {
|
||||
switch {
|
||||
case u.Username != "":
|
||||
return fmt.Sprintf("@%s", u.Username)
|
||||
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
||||
|
||||
case u.FirstName != "":
|
||||
return u.FirstName
|
||||
|
||||
case u.LastName != "":
|
||||
return u.LastName
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%d", u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
|
||||
// такового нет, то возвращает ID.
|
||||
func ConstructPartialChatUsername(c *echotron.Chat) string {
|
||||
switch {
|
||||
case c.Username != "":
|
||||
return fmt.Sprintf("@%s", c.Username)
|
||||
|
||||
case c.FirstName != "" && c.LastName != "":
|
||||
return fmt.Sprintf("%s %s", c.FirstName, c.LastName)
|
||||
|
||||
case c.FirstName != "":
|
||||
return c.FirstName
|
||||
|
||||
case c.LastName != "":
|
||||
return c.LastName
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%d", c.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// ConstructUserFirstLastName Пытается найти и вытащить first name и last name пользователя, если не получается, то
|
||||
// вначале пытается фоллбэчиться на first name, потом на last name, потом на username.
|
||||
func ConstructUserFirstLastName(u *echotron.User) string {
|
||||
var user string
|
||||
|
||||
switch {
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
user = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
||||
case u.FirstName != "":
|
||||
user = u.FirstName
|
||||
case u.LastName != "":
|
||||
user = u.LastName
|
||||
case u.Username != "":
|
||||
user = fmt.Sprintf("@%s", u.Username)
|
||||
default:
|
||||
user = fmt.Sprintf("%d", u.ID)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// ConstructTelegramHighlightName генерирует имя пользователя, которое триггерит на стороне клиент ивент меншена
|
||||
// определённого пользователя.
|
||||
func ConstructTelegramHighlightName(u *echotron.User) string {
|
||||
var (
|
||||
username string
|
||||
link string
|
||||
)
|
||||
|
||||
link = fmt.Sprintf("tg://user?id=%d", u.ID)
|
||||
|
||||
// Тут мы предполагаем, что как минимум либо firstname либо lastname либо username всегда есть. По сути так оно и
|
||||
// должно быть.
|
||||
switch {
|
||||
case u.FirstName != "" && u.LastName != "":
|
||||
username = fmt.Sprintf("%s %s", u.FirstName, u.LastName)
|
||||
case u.FirstName != "":
|
||||
username = u.FirstName
|
||||
case u.LastName != "":
|
||||
username = u.LastName
|
||||
default:
|
||||
username = u.Username
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s](%s)", username, link)
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
@@ -1,122 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/cockroachdb/errors/oserror"
|
||||
"github.com/cockroachdb/pebble"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StoreKV сохраняет в указанной бд ключ и значение.
|
||||
func StoreKV(db *pebble.DB, key string, value string) error {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
vArray = []byte(value)
|
||||
)
|
||||
|
||||
err := db.Set(kArray, vArray, pebble.Sync)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FetchV достаёт значение по ключу.
|
||||
func FetchV(db *pebble.DB, key string) (string, error) {
|
||||
var (
|
||||
kArray = []byte(key)
|
||||
|
||||
vArray []byte
|
||||
valueString = ""
|
||||
)
|
||||
|
||||
vArray, closer, err := db.Get(kArray)
|
||||
|
||||
if err != nil {
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
valueString = string(vArray)
|
||||
err = closer.Close()
|
||||
|
||||
return valueString, err
|
||||
}
|
||||
|
||||
// getSetting достаёт настройку из БД с настройками.
|
||||
func GetSetting(chatID string, setting string) string {
|
||||
var err error
|
||||
|
||||
chatHash := sha256.Sum256([]byte(chatID))
|
||||
database := fmt.Sprintf("settings_db/%x", chatHash)
|
||||
|
||||
// Если БД не открыта, откроем её
|
||||
if _, ok := settingsDB[database]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||
options.L0CompactionFileThreshold = 8
|
||||
settingsDB[database], err = pebble.Open(config.DataDir+"/"+database, &options)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
value, err := FetchV(settingsDB[database], setting)
|
||||
|
||||
// Если из базы ничего не вынулось, по каким-то причинам, то просто вернём пустую строку.
|
||||
if err != nil {
|
||||
if errors.Is(err, pebble.ErrNotFound) {
|
||||
log.Debugf("Unable to get value for %s: no record found in db %s", setting, database)
|
||||
} else if errors.Is(err, fs.ErrNotExist) {
|
||||
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
|
||||
} else if errors.Is(err, oserror.ErrNotExist) {
|
||||
log.Debugf("Unable to get value for %s: db dir %s does not exist", setting, database)
|
||||
} else {
|
||||
log.Errorf("Unable to get value for %s in db dir %s: %s", setting, database, err)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// saveSetting сохраняет настройку в БД с настройками.
|
||||
func GaveSetting(chatID string, setting string, value string) error {
|
||||
var (
|
||||
chatHash = sha256.Sum256([]byte(chatID))
|
||||
database = fmt.Sprintf("settings_db/%x", chatHash)
|
||||
err error
|
||||
)
|
||||
|
||||
// Если БД не открыта, откроем её.
|
||||
if _, ok := settingsDB[database]; !ok {
|
||||
var options pebble.Options
|
||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||
options.L0CompactionFileThreshold = 8
|
||||
|
||||
settingsDB[database], err = pebble.Open(config.DataDir+"/"+database, &options)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open settings db, %s: %s\n", database, err)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := StoreKV(settingsDB[database], setting, value); err != nil {
|
||||
log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
14
telegram.go
14
telegram.go
@@ -1,14 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/NicoNex/echotron/v3"
|
||||
)
|
||||
|
||||
// telega основная горутинка, реализующая бота.
|
||||
func telega(c myConfig) {
|
||||
tg = echotron.NewAPI(c.Telegram.Token)
|
||||
|
||||
for u := range echotron.PollingUpdates(c.Telegram.Token) {
|
||||
telegramMsgParser(u)
|
||||
}
|
||||
}
|
||||
66
types.go
66
types.go
@@ -1,66 +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"`
|
||||
}
|
||||
211
util.go
211
util.go
@@ -1,211 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/hjson/hjson-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// readConfig считывает и парсит конфиг-файл.
|
||||
func readConfig() {
|
||||
configLoaded := false
|
||||
executablePath, err := os.Executable()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get current executable path: %s", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
fileInfo, err := os.Stat(location)
|
||||
|
||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||
if fileInfo.Size() > 65535 {
|
||||
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
buf, err := os.ReadFile(location)
|
||||
|
||||
// Не удалось прочитать, попробуем следующего кандидата.
|
||||
if err != nil {
|
||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
|
||||
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
|
||||
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
|
||||
var (
|
||||
sampleConfig myConfig
|
||||
tmp map[string]interface{}
|
||||
)
|
||||
|
||||
err = hjson.Unmarshal(buf, &tmp)
|
||||
|
||||
// Не удалось распарсить - попробуем следующего кандидата.
|
||||
if err != nil {
|
||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
tmpjson, err := json.Marshal(tmp)
|
||||
|
||||
// Не удалось преобразовать map-ку в json.
|
||||
if err != nil {
|
||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
|
||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Валидируем значения из конфига.
|
||||
if sampleConfig.Redis.Server == "" {
|
||||
sampleConfig.Redis.Server = "localhost"
|
||||
|
||||
log.Infof("Redis server is not defined in config, using localhost")
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.Port == 0 {
|
||||
sampleConfig.Redis.Port = 6379
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.Channel == "" {
|
||||
log.Errorf("Channel field in config file %s must be set", location)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if sampleConfig.Redis.MyChannel == "" {
|
||||
log.Errorf("My_channel field in config file %s must be set", location)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if sampleConfig.Loglevel == "" {
|
||||
sampleConfig.Loglevel = "info"
|
||||
}
|
||||
|
||||
// sampleConfig.Log = "" if not set
|
||||
|
||||
if sampleConfig.Csign == "" {
|
||||
log.Errorf("Csign field in config file %s must be set", location)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if sampleConfig.ForwardsMax == 0 {
|
||||
sampleConfig.ForwardsMax = forwardMax
|
||||
}
|
||||
|
||||
if sampleConfig.DataDir == "" {
|
||||
log.Errorf("Data_dir field in config file %s must be set", location)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
config = sampleConfig
|
||||
configLoaded = true
|
||||
|
||||
log.Infof("Using %s as config file", location)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if !configLoaded {
|
||||
log.Error("Config was not loaded! Refusing to start.")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// sigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения.
|
||||
func sigHandler() {
|
||||
var err error
|
||||
|
||||
for {
|
||||
var s = <-sigChan
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
log.Infoln("Got SIGINT, quitting")
|
||||
case syscall.SIGTERM:
|
||||
log.Infoln("Got SIGTERM, quitting")
|
||||
case syscall.SIGQUIT:
|
||||
log.Infoln("Got SIGQUIT, quitting")
|
||||
|
||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true.
|
||||
shutdown = true
|
||||
|
||||
// Отпишемся от всех каналов и закроем коннект к редиске.
|
||||
if err = subscriber.Unsubscribe(ctx); err != nil {
|
||||
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
|
||||
} else {
|
||||
log.Debug("Unsubscribe from all redis channels")
|
||||
}
|
||||
|
||||
if err = subscriber.Close(); err != nil {
|
||||
log.Errorf("Unable to close redis connection cleanly: %s", err)
|
||||
} else {
|
||||
log.Debug("Close redis connection")
|
||||
}
|
||||
|
||||
// close all telego stuff.
|
||||
resp, err := tg.LogOut()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to send LogOut() to Telegram Bot API server")
|
||||
}
|
||||
|
||||
if !resp.Ok {
|
||||
log.Errorf("Telegram Bot API returns an error on LogOut(): %d, %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
|
||||
resp, err = tg.Close()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Unable to send Close() to Telegram Bot API server")
|
||||
}
|
||||
|
||||
if !resp.Ok {
|
||||
log.Errorf("Telegram Bot API returns an error on Close(): %d, %s", resp.ErrorCode, resp.Description)
|
||||
}
|
||||
|
||||
if len(settingsDB) > 0 {
|
||||
log.Debug("Closing runtime bot settings db")
|
||||
|
||||
for _, db := range settingsDB {
|
||||
_ = db.Close()
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||
2
vendor/github.com/NicoNex/echotron/v3/README.md
generated
vendored
2
vendor/github.com/NicoNex/echotron/v3/README.md
generated
vendored
@@ -1,4 +1,4 @@
|
||||
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [](https://golang.org/) [](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [](https://github.com/NicoNex/echotron/blob/master/LICENSE) [](https://coveralls.io/github/NicoNex/echotron?branch=master) [](https://github.com/avelino/awesome-go) [](https://t.me/echotronnews) |
|
||||
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [](https://golang.org/) [](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [](https://codecov.io/gh/NicoNex/echotron) [](https://github.com/NicoNex/echotron/blob/master/LICENSE) [](https://github.com/avelino/awesome-go) [](https://t.me/echotronnews) |
|
||||
| :------: |
|
||||
|
||||
**Echotron** is an elegant and concurrent library for the Telegram bot API in Go.
|
||||
|
||||
217
vendor/github.com/NicoNex/echotron/v3/api.go
generated
vendored
217
vendor/github.com/NicoNex/echotron/v3/api.go
generated
vendored
@@ -27,6 +27,7 @@ import (
|
||||
|
||||
// API is the object that contains all the functions that wrap those of the Telegram Bot API.
|
||||
type API struct {
|
||||
client *client
|
||||
token string
|
||||
base string
|
||||
}
|
||||
@@ -36,12 +37,22 @@ func NewAPI(token string) API {
|
||||
return API{
|
||||
token: token,
|
||||
base: fmt.Sprintf("https://api.telegram.org/bot%s/", token),
|
||||
client: lclient,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocalAPI is like NewAPI but allows to use a local API server.
|
||||
func NewLocalAPI(url, token string) API {
|
||||
return API{
|
||||
token: token,
|
||||
base: url,
|
||||
client: lclient,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUpdates is used to receive incoming updates using long polling.
|
||||
func (a API) GetUpdates(opts *UpdateOptions) (res APIResponseUpdate, err error) {
|
||||
return get[APIResponseUpdate](a.base, "getUpdates", urlValues(opts))
|
||||
return res, a.client.get(a.base, "getUpdates", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// SetWebhook is used to specify a url and receive incoming updates via an outgoing webhook.
|
||||
@@ -60,7 +71,7 @@ func (a API) SetWebhook(webhookURL string, dropPendingUpdates bool, opts *Webhoo
|
||||
addValues(vals, opts)
|
||||
url = fmt.Sprintf("%s?%s", strings.TrimSuffix(url, "/"), vals.Encode())
|
||||
|
||||
cnt, err := sendPostForm(url, keyVal)
|
||||
cnt, err := a.client.doPostForm(url, keyVal)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -78,17 +89,17 @@ func (a API) DeleteWebhook(dropPendingUpdates bool) (res APIResponseBase, err er
|
||||
var vals = make(url.Values)
|
||||
vals.Set("drop_pending_updates", btoa(dropPendingUpdates))
|
||||
|
||||
return get[APIResponseBase](a.base, "deleteWebhook", vals)
|
||||
return res, a.client.get(a.base, "deleteWebhook", vals, &res)
|
||||
}
|
||||
|
||||
// GetWebhookInfo is used to get current webhook status.
|
||||
func (a API) GetWebhookInfo() (res APIResponseWebhook, err error) {
|
||||
return get[APIResponseWebhook](a.base, "getWebhookInfo", nil)
|
||||
return res, a.client.get(a.base, "getWebhookInfo", nil, &res)
|
||||
}
|
||||
|
||||
// GetMe is a simple method for testing your bot's auth token.
|
||||
func (a API) GetMe() (res APIResponseUser, err error) {
|
||||
return get[APIResponseUser](a.base, "getMe", nil)
|
||||
return res, a.client.get(a.base, "getMe", nil, &res)
|
||||
}
|
||||
|
||||
// LogOut is used to log out from the cloud Bot API server before launching the bot locally.
|
||||
@@ -96,14 +107,14 @@ func (a API) GetMe() (res APIResponseUser, err error) {
|
||||
// After a successful call, you can immediately log in on a local server,
|
||||
// but will not be able to log in back to the cloud Bot API server for 10 minutes.
|
||||
func (a API) LogOut() (res APIResponseBool, err error) {
|
||||
return get[APIResponseBool](a.base, "logOut", nil)
|
||||
return res, a.client.get(a.base, "logOut", nil, &res)
|
||||
}
|
||||
|
||||
// Close is used to close the bot instance before moving it from one local server to another.
|
||||
// You need to delete the webhook before calling this method to ensure that the bot isn't launched again after server restart.
|
||||
// The method will return error 429 in the first 10 minutes after the bot is launched.
|
||||
func (a API) Close() (res APIResponseBool, err error) {
|
||||
return get[APIResponseBool](a.base, "close", nil)
|
||||
return res, a.client.get(a.base, "close", nil, &res)
|
||||
}
|
||||
|
||||
// SendMessage is used to send text messages.
|
||||
@@ -112,7 +123,7 @@ func (a API) SendMessage(text string, chatID int64, opts *MessageOptions) (res A
|
||||
|
||||
vals.Set("text", text)
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseMessage](a.base, "sendMessage", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendMessage", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// ForwardMessage is used to forward messages of any kind.
|
||||
@@ -123,7 +134,7 @@ func (a API) ForwardMessage(chatID, fromChatID int64, messageID int, opts *Forwa
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("from_chat_id", itoa(fromChatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponseMessage](a.base, "forwardMessage", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "forwardMessage", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// ForwardMessages is used to forward multiple messages of any kind.
|
||||
@@ -141,7 +152,7 @@ func (a API) ForwardMessages(chatID, fromChatID int64, messageIDs []int, opts *F
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("from_chat_id", itoa(fromChatID))
|
||||
vals.Set("message_ids", string(msgIDs))
|
||||
return get[APIResponseMessageIDs](a.base, "forwardMessages", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "forwardMessages", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// CopyMessage is used to copy messages of any kind.
|
||||
@@ -154,7 +165,7 @@ func (a API) CopyMessage(chatID, fromChatID int64, messageID int, opts *CopyOpti
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("from_chat_id", itoa(fromChatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponseMessageID](a.base, "copyMessage", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "copyMessage", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// CopyMessages is used to copy messages of any kind.
|
||||
@@ -174,7 +185,7 @@ func (a API) CopyMessages(chatID, fromChatID int64, messageIDs []int, opts *Copy
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("from_chat_id", itoa(fromChatID))
|
||||
vals.Set("message_ids", string(msgIDs))
|
||||
return get[APIResponseMessageIDs](a.base, "copyMessages", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "copyMessages", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendPhoto is used to send photos.
|
||||
@@ -182,7 +193,7 @@ func (a API) SendPhoto(file InputFile, chatID int64, opts *PhotoOptions) (res AP
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendPhoto", "photo", file, InputFile{}, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendPhoto", "photo", file, InputFile{}, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendAudio is used to send audio files,
|
||||
@@ -199,7 +210,7 @@ func (a API) SendAudio(file InputFile, chatID int64, opts *AudioOptions) (res AP
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendAudio", "audio", file, thumbnail, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendAudio", "audio", file, thumbnail, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendDocument is used to send general files.
|
||||
@@ -214,7 +225,7 @@ func (a API) SendDocument(file InputFile, chatID int64, opts *DocumentOptions) (
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendDocument", "document", file, thumbnail, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendDocument", "document", file, thumbnail, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendVideo is used to send video files.
|
||||
@@ -230,7 +241,7 @@ func (a API) SendVideo(file InputFile, chatID int64, opts *VideoOptions) (res AP
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendVideo", "video", file, thumbnail, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendVideo", "video", file, thumbnail, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendAnimation is used to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
|
||||
@@ -245,7 +256,7 @@ func (a API) SendAnimation(file InputFile, chatID int64, opts *AnimationOptions)
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendAnimation", "animation", file, thumbnail, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendAnimation", "animation", file, thumbnail, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendVoice is used to send audio files, if you want Telegram clients to display the file as a playable voice message.
|
||||
@@ -254,7 +265,7 @@ func (a API) SendVoice(file InputFile, chatID int64, opts *VoiceOptions) (res AP
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendVoice", "voice", file, InputFile{}, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendVoice", "voice", file, InputFile{}, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendVideoNote is used to send video messages.
|
||||
@@ -269,7 +280,7 @@ func (a API) SendVideoNote(file InputFile, chatID int64, opts *VideoNoteOptions)
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseMessage](a.base, "sendVideoNote", "video_note", file, thumbnail, addValues(vals, opts))
|
||||
return res, a.client.postFile(a.base, "sendVideoNote", "video_note", file, thumbnail, addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendMediaGroup is used to send a group of photos, videos, documents or audios as an album.
|
||||
@@ -278,7 +289,7 @@ func (a API) SendMediaGroup(chatID int64, media []GroupableInputMedia, opts *Med
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postMedia[APIResponseMessageArray](a.base, "sendMediaGroup", false, addValues(vals, opts), toInputMedia(media)...)
|
||||
return res, a.client.postMedia(a.base, "sendMediaGroup", false, addValues(vals, opts), &res, toInputMedia(media)...)
|
||||
}
|
||||
|
||||
// SendLocation is used to send point on the map.
|
||||
@@ -288,7 +299,7 @@ func (a API) SendLocation(chatID int64, latitude, longitude float64, opts *Locat
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("latitude", ftoa(latitude))
|
||||
vals.Set("longitude", ftoa(longitude))
|
||||
return get[APIResponseMessage](a.base, "sendLocation", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendLocation", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// EditMessageLiveLocation is used to edit live location messages.
|
||||
@@ -298,12 +309,12 @@ func (a API) EditMessageLiveLocation(msg MessageIDOptions, latitude, longitude f
|
||||
|
||||
vals.Set("latitude", ftoa(latitude))
|
||||
vals.Set("longitude", ftoa(longitude))
|
||||
return get[APIResponseMessage](a.base, "editMessageLiveLocation", addValues(addValues(vals, msg), opts))
|
||||
return res, a.client.get(a.base, "editMessageLiveLocation", addValues(addValues(vals, msg), opts), &res)
|
||||
}
|
||||
|
||||
// StopMessageLiveLocation is used to stop updating a live location message before `LivePeriod` expires.
|
||||
func (a API) StopMessageLiveLocation(msg MessageIDOptions, opts *MessageReplyMarkup) (res APIResponseMessage, err error) {
|
||||
return get[APIResponseMessage](a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts))
|
||||
func (a API) StopMessageLiveLocation(msg MessageIDOptions, opts *StopLocationOptions) (res APIResponseMessage, err error) {
|
||||
return res, a.client.get(a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts), &res)
|
||||
}
|
||||
|
||||
// SendVenue is used to send information about a venue.
|
||||
@@ -315,7 +326,7 @@ func (a API) SendVenue(chatID int64, latitude, longitude float64, title, address
|
||||
vals.Set("longitude", ftoa(longitude))
|
||||
vals.Set("title", title)
|
||||
vals.Set("address", address)
|
||||
return get[APIResponseMessage](a.base, "sendVenue", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendVenue", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendContact is used to send phone contacts.
|
||||
@@ -325,11 +336,11 @@ func (a API) SendContact(phoneNumber, firstName string, chatID int64, opts *Cont
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("phone_number", phoneNumber)
|
||||
vals.Set("first_name", firstName)
|
||||
return get[APIResponseMessage](a.base, "sendContact", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendContact", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendPoll is used to send a native poll.
|
||||
func (a API) SendPoll(chatID int64, question string, options []string, opts *PollOptions) (res APIResponseMessage, err error) {
|
||||
func (a API) SendPoll(chatID int64, question string, options []InputPollOption, opts *PollOptions) (res APIResponseMessage, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
pollOpts, err := json.Marshal(options)
|
||||
@@ -340,7 +351,7 @@ func (a API) SendPoll(chatID int64, question string, options []string, opts *Pol
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("question", question)
|
||||
vals.Set("options", string(pollOpts))
|
||||
return get[APIResponseMessage](a.base, "sendPoll", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendPoll", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendDice is used to send an animated emoji that will display a random value.
|
||||
@@ -349,7 +360,7 @@ func (a API) SendDice(chatID int64, emoji DiceEmoji, opts *BaseOptions) (res API
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("emoji", string(emoji))
|
||||
return get[APIResponseMessage](a.base, "sendDice", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendDice", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SendChatAction is used to tell the user that something is happening on the bot's side.
|
||||
@@ -359,7 +370,7 @@ func (a API) SendChatAction(action ChatAction, chatID int64, opts *ChatActionOpt
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("action", string(action))
|
||||
return get[APIResponseBool](a.base, "sendChatAction", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendChatAction", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SetMessageReaction is used to change the chosen reactions on a message.
|
||||
@@ -371,7 +382,7 @@ func (a API) SetMessageReaction(chatID int64, messageID int, opts *MessageReacti
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponseBool](a.base, "setMessageReaction", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "setMessageReaction", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// GetUserProfilePhotos is used to get a list of profile pictures for a user.
|
||||
@@ -379,7 +390,7 @@ func (a API) GetUserProfilePhotos(userID int64, opts *UserProfileOptions) (res A
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseUserProfile](a.base, "getUserProfilePhotos", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "getUserProfilePhotos", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// GetFile returns the basic info about a file and prepares it for downloading.
|
||||
@@ -391,14 +402,14 @@ func (a API) GetFile(fileID string) (res APIResponseFile, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("file_id", fileID)
|
||||
return get[APIResponseFile](a.base, "getFile", vals)
|
||||
return res, a.client.get(a.base, "getFile", vals, &res)
|
||||
}
|
||||
|
||||
// DownloadFile returns the bytes of the file corresponding to the given filePath.
|
||||
// This function is callable for at least 1 hour since the call to GetFile.
|
||||
// When the download expires a new one can be requested by calling GetFile again.
|
||||
func (a API) DownloadFile(filePath string) ([]byte, error) {
|
||||
return sendGetRequest(fmt.Sprintf(
|
||||
return a.client.doGet(fmt.Sprintf(
|
||||
"https://api.telegram.org/file/bot%s/%s",
|
||||
a.token,
|
||||
filePath,
|
||||
@@ -414,7 +425,7 @@ func (a API) BanChatMember(chatID, userID int64, opts *BanOptions) (res APIRespo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseBool](a.base, "banChatMember", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "banChatMember", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// UnbanChatMember is used to unban a previously banned user in a supergroup or channel.
|
||||
@@ -428,7 +439,7 @@ func (a API) UnbanChatMember(chatID, userID int64, opts *UnbanOptions) (res APIR
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseBool](a.base, "unbanChatMember", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "unbanChatMember", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// RestrictChatMember is used to restrict a user in a supergroup.
|
||||
@@ -436,15 +447,15 @@ func (a API) UnbanChatMember(chatID, userID int64, opts *UnbanOptions) (res APIR
|
||||
func (a API) RestrictChatMember(chatID, userID int64, permissions ChatPermissions, opts *RestrictOptions) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
perm, err := serializePerms(permissions)
|
||||
perm, err := json.Marshal(permissions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("permissions", perm)
|
||||
return get[APIResponseBool](a.base, "restrictChatMember", addValues(vals, opts))
|
||||
vals.Set("permissions", string(perm))
|
||||
return res, a.client.get(a.base, "restrictChatMember", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// PromoteChatMember is used to promote or demote a user in a supergroup or a channel.
|
||||
@@ -454,7 +465,7 @@ func (a API) PromoteChatMember(chatID, userID int64, opts *PromoteOptions) (res
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseBool](a.base, "promoteChatMember", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "promoteChatMember", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SetChatAdministratorCustomTitle is used to set a custom title for an administrator in a supergroup promoted by the bot.
|
||||
@@ -464,7 +475,7 @@ func (a API) SetChatAdministratorCustomTitle(chatID, userID int64, customTitle s
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("custom_title", customTitle)
|
||||
return get[APIResponseBool](a.base, "setChatAdministratorCustomTitle", vals)
|
||||
return res, a.client.get(a.base, "setChatAdministratorCustomTitle", vals, &res)
|
||||
}
|
||||
|
||||
// BanChatSenderChat is used to ban a channel chat in a supergroup or a channel.
|
||||
@@ -475,7 +486,7 @@ func (a API) BanChatSenderChat(chatID, senderChatID int64) (res APIResponseBool,
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("sender_chat_id", itoa(senderChatID))
|
||||
return get[APIResponseBool](a.base, "banChatSenderChat", vals)
|
||||
return res, a.client.get(a.base, "banChatSenderChat", vals, &res)
|
||||
}
|
||||
|
||||
// UnbanChatSenderChat is used to unban a previously channel chat in a supergroup or channel.
|
||||
@@ -485,7 +496,7 @@ func (a API) UnbanChatSenderChat(chatID, senderChatID int64) (res APIResponseBoo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("sender_chat_id", itoa(senderChatID))
|
||||
return get[APIResponseBool](a.base, "unbanChatSenderChat", vals)
|
||||
return res, a.client.get(a.base, "unbanChatSenderChat", vals, &res)
|
||||
}
|
||||
|
||||
// SetChatPermissions is used to set default chat permissions for all members.
|
||||
@@ -493,14 +504,14 @@ func (a API) UnbanChatSenderChat(chatID, senderChatID int64) (res APIResponseBoo
|
||||
func (a API) SetChatPermissions(chatID int64, permissions ChatPermissions, opts *ChatPermissionsOptions) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
perm, err := serializePerms(permissions)
|
||||
perm, err := json.Marshal(permissions)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("permissions", perm)
|
||||
return get[APIResponseBool](a.base, "setChatPermissions", addValues(vals, opts))
|
||||
vals.Set("permissions", string(perm))
|
||||
return res, a.client.get(a.base, "setChatPermissions", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// ExportChatInviteLink is used to generate a new primary invite link for a chat;
|
||||
@@ -510,7 +521,7 @@ func (a API) ExportChatInviteLink(chatID int64) (res APIResponseString, err erro
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseString](a.base, "exportChatInviteLink", vals)
|
||||
return res, a.client.get(a.base, "exportChatInviteLink", vals, &res)
|
||||
}
|
||||
|
||||
// CreateChatInviteLink is used to create an additional invite link for a chat.
|
||||
@@ -520,7 +531,7 @@ func (a API) CreateChatInviteLink(chatID int64, opts *InviteLinkOptions) (res AP
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseInviteLink](a.base, "createChatInviteLink", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "createChatInviteLink", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// EditChatInviteLink is used to edit a non-primary invite link created by the bot.
|
||||
@@ -530,7 +541,7 @@ func (a API) EditChatInviteLink(chatID int64, inviteLink string, opts *InviteLin
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("invite_link", inviteLink)
|
||||
return get[APIResponseInviteLink](a.base, "editChatInviteLink", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "editChatInviteLink", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// RevokeChatInviteLink is used to revoke an invite link created by the bot.
|
||||
@@ -541,7 +552,7 @@ func (a API) RevokeChatInviteLink(chatID int64, inviteLink string) (res APIRespo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("invite_link", inviteLink)
|
||||
return get[APIResponseInviteLink](a.base, "editChatInviteLink", vals)
|
||||
return res, a.client.get(a.base, "editChatInviteLink", vals, &res)
|
||||
}
|
||||
|
||||
// ApproveChatJoinRequest is used to approve a chat join request.
|
||||
@@ -551,7 +562,7 @@ func (a API) ApproveChatJoinRequest(chatID, userID int64) (res APIResponseBool,
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseBool](a.base, "approveChatJoinRequest", vals)
|
||||
return res, a.client.get(a.base, "approveChatJoinRequest", vals, &res)
|
||||
}
|
||||
|
||||
// DeclineChatJoinRequest is used to decline a chat join request.
|
||||
@@ -561,7 +572,7 @@ func (a API) DeclineChatJoinRequest(chatID, userID int64) (res APIResponseBool,
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseBool](a.base, "declineChatJoinRequest", vals)
|
||||
return res, a.client.get(a.base, "declineChatJoinRequest", vals, &res)
|
||||
}
|
||||
|
||||
// SetChatPhoto is used to set a new profile photo for the chat.
|
||||
@@ -571,7 +582,7 @@ func (a API) SetChatPhoto(file InputFile, chatID int64) (res APIResponseBool, er
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return postFile[APIResponseBool](a.base, "setChatPhoto", "photo", file, InputFile{}, vals)
|
||||
return res, a.client.postFile(a.base, "setChatPhoto", "photo", file, InputFile{}, vals, &res)
|
||||
}
|
||||
|
||||
// DeleteChatPhoto is used to delete a chat photo.
|
||||
@@ -581,7 +592,7 @@ func (a API) DeleteChatPhoto(chatID int64) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "deleteChatPhoto", vals)
|
||||
return res, a.client.get(a.base, "deleteChatPhoto", vals, &res)
|
||||
}
|
||||
|
||||
// SetChatTitle is used to change the title of a chat.
|
||||
@@ -592,7 +603,7 @@ func (a API) SetChatTitle(chatID int64, title string) (res APIResponseBool, err
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("title", title)
|
||||
return get[APIResponseBool](a.base, "setChatTitle", vals)
|
||||
return res, a.client.get(a.base, "setChatTitle", vals, &res)
|
||||
}
|
||||
|
||||
// SetChatDescription is used to change the description of a group, a supergroup or a channel.
|
||||
@@ -602,7 +613,7 @@ func (a API) SetChatDescription(chatID int64, description string) (res APIRespon
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("description", description)
|
||||
return get[APIResponseBool](a.base, "setChatDescription", vals)
|
||||
return res, a.client.get(a.base, "setChatDescription", vals, &res)
|
||||
}
|
||||
|
||||
// PinChatMessage is used to add a message to the list of pinned messages in the chat.
|
||||
@@ -613,7 +624,7 @@ func (a API) PinChatMessage(chatID int64, messageID int, opts *PinMessageOptions
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponseBool](a.base, "pinChatMessage", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "pinChatMessage", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// UnpinChatMessage is used to remove a message from the list of pinned messages in the chat.
|
||||
@@ -624,7 +635,7 @@ func (a API) UnpinChatMessage(chatID int64, messageID int) (res APIResponseBool,
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponseBool](a.base, "unpinChatMessage", vals)
|
||||
return res, a.client.get(a.base, "unpinChatMessage", vals, &res)
|
||||
}
|
||||
|
||||
// UnpinAllChatMessages is used to clear the list of pinned messages in a chat.
|
||||
@@ -634,7 +645,7 @@ func (a API) UnpinAllChatMessages(chatID int64) (res APIResponseBool, err error)
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "unpinAllChatMessages", vals)
|
||||
return res, a.client.get(a.base, "unpinAllChatMessages", vals, &res)
|
||||
}
|
||||
|
||||
// LeaveChat is used to make the bot leave a group, supergroup or channel.
|
||||
@@ -642,7 +653,7 @@ func (a API) LeaveChat(chatID int64) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "leaveChat", vals)
|
||||
return res, a.client.get(a.base, "leaveChat", vals, &res)
|
||||
}
|
||||
|
||||
// GetChat is used to get up to date information about the chat.
|
||||
@@ -651,7 +662,7 @@ func (a API) GetChat(chatID int64) (res APIResponseChat, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseChat](a.base, "getChat", vals)
|
||||
return res, a.client.get(a.base, "getChat", vals, &res)
|
||||
}
|
||||
|
||||
// GetChatAdministrators is used to get a list of administrators in a chat.
|
||||
@@ -659,7 +670,7 @@ func (a API) GetChatAdministrators(chatID int64) (res APIResponseAdministrators,
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseAdministrators](a.base, "getChatAdministrators", vals)
|
||||
return res, a.client.get(a.base, "getChatAdministrators", vals, &res)
|
||||
}
|
||||
|
||||
// GetChatMemberCount is used to get the number of members in a chat.
|
||||
@@ -667,7 +678,7 @@ func (a API) GetChatMemberCount(chatID int64) (res APIResponseInteger, err error
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseInteger](a.base, "getChatMemberCount", vals)
|
||||
return res, a.client.get(a.base, "getChatMemberCount", vals, &res)
|
||||
}
|
||||
|
||||
// GetChatMember is used to get information about a member of a chat.
|
||||
@@ -676,7 +687,7 @@ func (a API) GetChatMember(chatID, userID int64) (res APIResponseChatMember, err
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseChatMember](a.base, "getChatMember", vals)
|
||||
return res, a.client.get(a.base, "getChatMember", vals, &res)
|
||||
}
|
||||
|
||||
// SetChatStickerSet is used to set a new group sticker set for a supergroup.
|
||||
@@ -687,7 +698,7 @@ func (a API) SetChatStickerSet(chatID int64, stickerSetName string) (res APIResp
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("sticker_set_name", stickerSetName)
|
||||
return get[APIResponseBool](a.base, "setChatStickerSet", vals)
|
||||
return res, a.client.get(a.base, "setChatStickerSet", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteChatStickerSet is used to delete a group sticker set for a supergroup.
|
||||
@@ -697,7 +708,7 @@ func (a API) DeleteChatStickerSet(chatID int64) (res APIResponseBool, err error)
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "deleteChatStickerSet", vals)
|
||||
return res, a.client.get(a.base, "deleteChatStickerSet", vals, &res)
|
||||
}
|
||||
|
||||
// CreateForumTopic is used to create a topic in a forum supergroup chat.
|
||||
@@ -707,7 +718,7 @@ func (a API) CreateForumTopic(chatID int64, name string, opts *CreateTopicOption
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("name", name)
|
||||
return get[APIResponseForumTopic](a.base, "createForumTopic", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "createForumTopic", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// EditForumTopic is used to edit name and icon of a topic in a forum supergroup chat.
|
||||
@@ -717,7 +728,7 @@ func (a API) EditForumTopic(chatID, messageThreadID int64, opts *EditTopicOption
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_thread_id", itoa(messageThreadID))
|
||||
return get[APIResponseBool](a.base, "editForumTopic", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "editForumTopic", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// CloseForumTopic is used to close an open topic in a forum supergroup chat.
|
||||
@@ -727,7 +738,7 @@ func (a API) CloseForumTopic(chatID, messageThreadID int64) (res APIResponseBool
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_thread_id", itoa(messageThreadID))
|
||||
return get[APIResponseBool](a.base, "closeForumTopic", vals)
|
||||
return res, a.client.get(a.base, "closeForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// ReopenForumTopic is used to reopen a closed topic in a forum supergroup chat.
|
||||
@@ -737,7 +748,7 @@ func (a API) ReopenForumTopic(chatID, messageThreadID int64) (res APIResponseBoo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_thread_id", itoa(messageThreadID))
|
||||
return get[APIResponseBool](a.base, "reopenForumTopic", vals)
|
||||
return res, a.client.get(a.base, "reopenForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteForumTopic is used to delete a forum topic along with all its messages in a forum supergroup chat.
|
||||
@@ -747,7 +758,7 @@ func (a API) DeleteForumTopic(chatID, messageThreadID int64) (res APIResponseBoo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_thread_id", itoa(messageThreadID))
|
||||
return get[APIResponseBool](a.base, "deleteForumTopic", vals)
|
||||
return res, a.client.get(a.base, "deleteForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// UnpinAllForumTopicMessages is used to clear the list of pinned messages in a forum topic.
|
||||
@@ -757,7 +768,7 @@ func (a API) UnpinAllForumTopicMessages(chatID, messageThreadID int64) (res APIR
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_thread_id", itoa(messageThreadID))
|
||||
return get[APIResponseBool](a.base, "unpinAllForumTopicMessages", vals)
|
||||
return res, a.client.get(a.base, "unpinAllForumTopicMessages", vals, &res)
|
||||
}
|
||||
|
||||
// EditGeneralForumTopic is used to edit the name of the 'General' topic in a forum supergroup chat.
|
||||
@@ -767,7 +778,7 @@ func (a API) EditGeneralForumTopic(chatID int64, name string) (res APIResponseBo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("name", name)
|
||||
return get[APIResponseBool](a.base, "editGeneralForumTopic", vals)
|
||||
return res, a.client.get(a.base, "editGeneralForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// CloseGeneralForumTopic is used to close an open 'General' topic in a forum supergroup chat.
|
||||
@@ -776,7 +787,7 @@ func (a API) CloseGeneralForumTopic(chatID int64) (res APIResponseBool, err erro
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "closeGeneralForumTopic", vals)
|
||||
return res, a.client.get(a.base, "closeGeneralForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// ReopenGeneralForumTopic is used to reopen a closed 'General' topic in a forum supergroup chat.
|
||||
@@ -786,7 +797,7 @@ func (a API) ReopenGeneralForumTopic(chatID int64) (res APIResponseBool, err err
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "reopenGeneralForumTopic", vals)
|
||||
return res, a.client.get(a.base, "reopenGeneralForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// HideGeneralForumTopic is used to hide the 'General' topic in a forum supergroup chat.
|
||||
@@ -796,7 +807,7 @@ func (a API) HideGeneralForumTopic(chatID int64) (res APIResponseBool, err error
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "hideGeneralForumTopic", vals)
|
||||
return res, a.client.get(a.base, "hideGeneralForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// UnhideGeneralForumTopic is used to unhide the 'General' topic in a forum supergroup chat.
|
||||
@@ -805,7 +816,7 @@ func (a API) UnhideGeneralForumTopic(chatID int64) (res APIResponseBool, err err
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "unhideGeneralForumTopic", vals)
|
||||
return res, a.client.get(a.base, "unhideGeneralForumTopic", vals, &res)
|
||||
}
|
||||
|
||||
// UnpinAllGeneralForumTopicMessages is used to clear the list of pinned messages in a General forum topic.
|
||||
@@ -814,7 +825,7 @@ func (a API) UnpinAllGeneralForumTopicMessages(chatID int64) (res APIResponseBoo
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseBool](a.base, "unpinAllGeneralForumTopicMessages", vals)
|
||||
return res, a.client.get(a.base, "unpinAllGeneralForumTopicMessages", vals, &res)
|
||||
}
|
||||
|
||||
// AnswerCallbackQuery is used to send answers to callback queries sent from inline keyboards.
|
||||
@@ -823,7 +834,7 @@ func (a API) AnswerCallbackQuery(callbackID string, opts *CallbackQueryOptions)
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("callback_query_id", callbackID)
|
||||
return get[APIResponseBool](a.base, "answerCallbackQuery", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "answerCallbackQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// GetUserChatBoosts is used to get the list of boosts added to a chat by a user.
|
||||
@@ -833,7 +844,15 @@ func (a API) GetUserChatBoosts(chatID, userID int64) (res APIResponseUserChatBoo
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseUserChatBoosts](a.base, "getUserChatBoosts", vals)
|
||||
return res, a.client.get(a.base, "getUserChatBoosts", vals, &res)
|
||||
}
|
||||
|
||||
// GetBusinessConnection is used to get information about the connection of the bot with a business account.
|
||||
func (a API) GetBusinessConnection(business_connection_id string) (res APIResponseBusinessConnection, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("business_connection_id", business_connection_id)
|
||||
return res, a.client.get(a.base, "getBusinessConnection", vals, &res)
|
||||
}
|
||||
|
||||
// SetMyCommands is used to change the list of the bot's commands for the given scope and user language.
|
||||
@@ -842,17 +861,17 @@ func (a API) SetMyCommands(opts *CommandOptions, commands ...BotCommand) (res AP
|
||||
|
||||
jsn, _ := json.Marshal(commands)
|
||||
vals.Set("commands", string(jsn))
|
||||
return get[APIResponseBool](a.base, "setMyCommands", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "setMyCommands", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// DeleteMyCommands is used to delete the list of the bot's commands for the given scope and user language.
|
||||
func (a API) DeleteMyCommands(opts *CommandOptions) (res APIResponseBool, err error) {
|
||||
return get[APIResponseBool](a.base, "deleteMyCommands", urlValues(opts))
|
||||
return res, a.client.get(a.base, "deleteMyCommands", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// GetMyCommands is used to get the current list of the bot's commands for the given scope and user language.
|
||||
func (a API) GetMyCommands(opts *CommandOptions) (res APIResponseCommands, err error) {
|
||||
return get[APIResponseCommands](a.base, "getMyCommands", urlValues(opts))
|
||||
return res, a.client.get(a.base, "getMyCommands", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// SetMyName is used to change the bot's name.
|
||||
@@ -861,7 +880,7 @@ func (a API) SetMyName(name, languageCode string) (res APIResponseBool, err erro
|
||||
|
||||
vals.Set("name", name)
|
||||
vals.Set("language_code", languageCode)
|
||||
return get[APIResponseBool](a.base, "setMyName", vals)
|
||||
return res, a.client.get(a.base, "setMyName", vals, &res)
|
||||
}
|
||||
|
||||
// GetMyName is used to get the current bot name for the given user language.
|
||||
@@ -869,7 +888,7 @@ func (a API) GetMyName(languageCode string) (res APIResponseBotName, err error)
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("language_code", languageCode)
|
||||
return get[APIResponseBotName](a.base, "getMyName", vals)
|
||||
return res, a.client.get(a.base, "getMyName", vals, &res)
|
||||
}
|
||||
|
||||
// SetMyDescription is used to to change the bot's description, which is shown in the chat with the bot if the chat is empty.
|
||||
@@ -878,7 +897,7 @@ func (a API) SetMyDescription(description, languageCode string) (res APIResponse
|
||||
|
||||
vals.Set("description", description)
|
||||
vals.Set("language_code", languageCode)
|
||||
return get[APIResponseBool](a.base, "setMyDescription", vals)
|
||||
return res, a.client.get(a.base, "setMyDescription", vals, &res)
|
||||
}
|
||||
|
||||
// GetMyDescription is used to get the current bot description for the given user language.
|
||||
@@ -886,7 +905,7 @@ func (a API) GetMyDescription(languageCode string) (res APIResponseBotDescriptio
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("language_code", languageCode)
|
||||
return get[APIResponseBotDescription](a.base, "getMyDescription", vals)
|
||||
return res, a.client.get(a.base, "getMyDescription", vals, &res)
|
||||
}
|
||||
|
||||
// SetMyShortDescription is used to to change the bot's short description,
|
||||
@@ -896,7 +915,7 @@ func (a API) SetMyShortDescription(shortDescription, languageCode string) (res A
|
||||
|
||||
vals.Set("short_description", shortDescription)
|
||||
vals.Set("language_code", languageCode)
|
||||
return get[APIResponseBool](a.base, "setMyShortDescription", vals)
|
||||
return res, a.client.get(a.base, "setMyShortDescription", vals, &res)
|
||||
}
|
||||
|
||||
// GetMyShortDescription is used to get the current bot short description for the given user language.
|
||||
@@ -904,7 +923,7 @@ func (a API) GetMyShortDescription(languageCode string) (res APIResponseBotShort
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("language_code", languageCode)
|
||||
return get[APIResponseBotShortDescription](a.base, "getMyDescription", vals)
|
||||
return res, a.client.get(a.base, "getMyDescription", vals, &res)
|
||||
}
|
||||
|
||||
// EditMessageText is used to edit text and game messages.
|
||||
@@ -912,12 +931,12 @@ func (a API) EditMessageText(text string, msg MessageIDOptions, opts *MessageTex
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("text", text)
|
||||
return get[APIResponseMessage](a.base, "editMessageText", addValues(addValues(vals, msg), opts))
|
||||
return res, a.client.get(a.base, "editMessageText", addValues(addValues(vals, msg), opts), &res)
|
||||
}
|
||||
|
||||
// EditMessageCaption is used to edit captions of messages.
|
||||
func (a API) EditMessageCaption(msg MessageIDOptions, opts *MessageCaptionOptions) (res APIResponseMessage, err error) {
|
||||
return get[APIResponseMessage](a.base, "editMessageCaption", addValues(urlValues(msg), opts))
|
||||
return res, a.client.get(a.base, "editMessageCaption", addValues(urlValues(msg), opts), &res)
|
||||
}
|
||||
|
||||
// EditMessageMedia is used to edit animation, audio, document, photo or video messages.
|
||||
@@ -925,22 +944,22 @@ func (a API) EditMessageCaption(msg MessageIDOptions, opts *MessageCaptionOption
|
||||
// only to a document for document albums and to a photo or a video otherwise.
|
||||
// When an inline message is edited, a new file can't be uploaded.
|
||||
// Use a previously uploaded file via its file_id or specify a URL.
|
||||
func (a API) EditMessageMedia(msg MessageIDOptions, media InputMedia, opts *MessageReplyMarkup) (res APIResponseMessage, err error) {
|
||||
return postMedia[APIResponseMessage](a.base, "editMessageMedia", true, addValues(urlValues(msg), opts), media)
|
||||
func (a API) EditMessageMedia(msg MessageIDOptions, media InputMedia, opts *MessageMediaOptions) (res APIResponseMessage, err error) {
|
||||
return res, a.client.postMedia(a.base, "editMessageMedia", true, addValues(urlValues(msg), opts), &res, media)
|
||||
}
|
||||
|
||||
// EditMessageReplyMarkup is used to edit only the reply markup of messages.
|
||||
func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkup) (res APIResponseMessage, err error) {
|
||||
return get[APIResponseMessage](a.base, "editMessageReplyMarkup", addValues(urlValues(msg), opts))
|
||||
func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkupOptions) (res APIResponseMessage, err error) {
|
||||
return res, a.client.get(a.base, "editMessageReplyMarkup", addValues(urlValues(msg), opts), &res)
|
||||
}
|
||||
|
||||
// StopPoll is used to stop a poll which was sent by the bot.
|
||||
func (a API) StopPoll(chatID int64, messageID int, opts *MessageReplyMarkup) (res APIResponsePoll, err error) {
|
||||
func (a API) StopPoll(chatID int64, messageID int, opts *StopPollOptions) (res APIResponsePoll, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponsePoll](a.base, "stopPoll", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "stopPoll", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// DeleteMessage is used to delete a message, including service messages, with the following limitations:
|
||||
@@ -956,7 +975,7 @@ func (a API) DeleteMessage(chatID int64, messageID int) (res APIResponseBase, er
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_id", itoa(int64(messageID)))
|
||||
return get[APIResponseBase](a.base, "deleteMessage", vals)
|
||||
return res, a.client.get(a.base, "deleteMessage", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteMessages is used to delete multiple messages simultaneously.
|
||||
@@ -971,5 +990,5 @@ func (a API) DeleteMessages(chatID int64, messageIDs []int) (res APIResponseBool
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("message_ids", string(msgIDs))
|
||||
return get[APIResponseBool](a.base, "deleteMessages", vals)
|
||||
return res, a.client.get(a.base, "deleteMessages", vals, &res)
|
||||
}
|
||||
|
||||
8
vendor/github.com/NicoNex/echotron/v3/chatadminrights.go
generated
vendored
8
vendor/github.com/NicoNex/echotron/v3/chatadminrights.go
generated
vendored
@@ -54,11 +54,11 @@ type GetMyDefaultAdministratorRightsOptions struct {
|
||||
// requested by the bot when it's added as an administrator to groups or channels.
|
||||
// These rights will be suggested to users, but they are are free to modify the list
|
||||
// before adding the bot.
|
||||
func (a API) SetMyDefaultAdministratorRights(opts SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
|
||||
return get[APIResponseBool](a.base, "setMyDefaultAdministratorRights", urlValues(opts))
|
||||
func (a API) SetMyDefaultAdministratorRights(opts *SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
|
||||
return res, a.client.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot.
|
||||
func (a API) GetMyDefaultAdministratorRights(opts GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
|
||||
return get[APIResponseChatAdministratorRights](a.base, "getMyDefaultAdministratorRights", urlValues(opts))
|
||||
func (a API) GetMyDefaultAdministratorRights(opts *GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
|
||||
return res, a.client.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
6
vendor/github.com/NicoNex/echotron/v3/games.go
generated
vendored
6
vendor/github.com/NicoNex/echotron/v3/games.go
generated
vendored
@@ -52,7 +52,7 @@ func (a API) SendGame(gameShortName string, chatID int64, opts *BaseOptions) (re
|
||||
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
vals.Set("game_short_name", gameShortName)
|
||||
return get[APIResponseMessage](a.base, "sendGame", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendGame", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// SetGameScore is used to set the score of the specified user in a game.
|
||||
@@ -61,7 +61,7 @@ func (a API) SetGameScore(userID int64, score int, msgID MessageIDOptions, opts
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("score", itoa(int64(score)))
|
||||
return get[APIResponseMessage](a.base, "setGameScore", addValues(addValues(vals, msgID), opts))
|
||||
return res, a.client.get(a.base, "setGameScore", addValues(addValues(vals, msgID), opts), &res)
|
||||
}
|
||||
|
||||
// GetGameHighScores is used to get data for high score tables.
|
||||
@@ -69,5 +69,5 @@ func (a API) GetGameHighScores(userID int64, opts MessageIDOptions) (res APIResp
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return get[APIResponseGameHighScore](a.base, "getGameHighScores", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "getGameHighScores", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
206
vendor/github.com/NicoNex/echotron/v3/helpers.go
generated
vendored
206
vendor/github.com/NicoNex/echotron/v3/helpers.go
generated
vendored
@@ -19,7 +19,6 @@
|
||||
package echotron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -27,6 +26,13 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// content contains a file's name, its type and its data.
|
||||
type content struct {
|
||||
fname string
|
||||
ftype string
|
||||
fdata []byte
|
||||
}
|
||||
|
||||
func check(r APIResponse) error {
|
||||
if b := r.Base(); !b.Ok {
|
||||
return &APIError{code: b.ErrorCode, desc: b.Description}
|
||||
@@ -109,123 +115,6 @@ func readFile(im InputFile) (content []byte, path string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
|
||||
var cnt []content
|
||||
|
||||
if file.id != "" {
|
||||
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
|
||||
} else if file.url != "" {
|
||||
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
|
||||
} else if c, e := toContent(fileType, file); e == nil {
|
||||
cnt = append(cnt, c)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
if c, e := toContent("thumbnail", thumbnail); e == nil {
|
||||
cnt = append(cnt, c)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
res, err = sendPostRequest(url, cnt...)
|
||||
} else {
|
||||
res, err = sendGetRequest(url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
|
||||
var (
|
||||
med []mediaEnvelope
|
||||
cnt []content
|
||||
jsn []byte
|
||||
)
|
||||
|
||||
for _, file := range files {
|
||||
var im mediaEnvelope
|
||||
var cntArr []content
|
||||
|
||||
media := file.media()
|
||||
thumbnail := file.thumbnail()
|
||||
|
||||
im, cntArr, err = processMedia(media, thumbnail)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
im.InputMedia = file
|
||||
|
||||
med = append(med, im)
|
||||
cnt = append(cnt, cntArr...)
|
||||
}
|
||||
|
||||
if editSingle {
|
||||
jsn, err = json.Marshal(med[0])
|
||||
} else {
|
||||
jsn, err = json.Marshal(med)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url = fmt.Sprintf("%s&media=%s", url, jsn)
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return sendPostRequest(url, cnt...)
|
||||
}
|
||||
|
||||
return sendGetRequest(url)
|
||||
}
|
||||
|
||||
func sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
|
||||
var (
|
||||
sti []stickerEnvelope
|
||||
cnt []content
|
||||
jsn []byte
|
||||
)
|
||||
|
||||
for _, s := range stickers {
|
||||
var se stickerEnvelope
|
||||
var cntArr []content
|
||||
|
||||
se, cntArr, err = processSticker(s.Sticker)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
se.InputSticker = s
|
||||
|
||||
sti = append(sti, se)
|
||||
cnt = append(cnt, cntArr...)
|
||||
}
|
||||
|
||||
if len(sti) == 1 {
|
||||
jsn, _ = json.Marshal(sti[0])
|
||||
url = fmt.Sprintf("%s&sticker=%s", url, jsn)
|
||||
} else {
|
||||
jsn, _ = json.Marshal(sti)
|
||||
url = fmt.Sprintf("%s&stickers=%s", url, jsn)
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return sendPostRequest(url, cnt...)
|
||||
}
|
||||
|
||||
return sendGetRequest(url)
|
||||
}
|
||||
|
||||
func serializePerms(permissions ChatPermissions) (string, error) {
|
||||
perm, err := json.Marshal(permissions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(perm), nil
|
||||
}
|
||||
|
||||
func toContent(ftype string, f InputFile) (content, error) {
|
||||
if f.path != "" && len(f.content) == 0 {
|
||||
var err error
|
||||
@@ -247,87 +136,6 @@ func toInputMedia(media []GroupableInputMedia) (ret []InputMedia) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func get[T APIResponse](base, endpoint string, vals url.Values) (res T, err error) {
|
||||
url, err := url.JoinPath(base, endpoint)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if vals != nil {
|
||||
if queries := vals.Encode(); queries != "" {
|
||||
url = fmt.Sprintf("%s?%s", url, queries)
|
||||
}
|
||||
}
|
||||
|
||||
cnt, err := sendGetRequest(url)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(cnt, &res); err != nil {
|
||||
return
|
||||
}
|
||||
err = check(res)
|
||||
return
|
||||
}
|
||||
|
||||
func postFile[T APIResponse](base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values) (res T, err error) {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
cnt, err := sendFile(file, thumbnail, url, fileType)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(cnt, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = check(res)
|
||||
return
|
||||
}
|
||||
|
||||
func postMedia[T APIResponse](base, endpoint string, editSingle bool, vals url.Values, files ...InputMedia) (res T, err error) {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
cnt, err := sendMediaFiles(url, editSingle, files...)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(cnt, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = check(res)
|
||||
return
|
||||
}
|
||||
|
||||
func postStickers[T APIResponse](base, endpoint string, vals url.Values, stickers ...InputSticker) (res T, err error) {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
cnt, err := sendStickers(url, stickers...)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(cnt, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = check(res)
|
||||
return
|
||||
}
|
||||
|
||||
func joinURL(base, endpoint string, vals url.Values) (addr string, err error) {
|
||||
addr, err = url.JoinPath(base, endpoint)
|
||||
if err != nil {
|
||||
|
||||
39
vendor/github.com/NicoNex/echotron/v3/inline.go
generated
vendored
39
vendor/github.com/NicoNex/echotron/v3/inline.go
generated
vendored
@@ -103,6 +103,7 @@ type InlineQueryResultPhoto struct {
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
PhotoHeight int `json:"photo_height,omitempty"`
|
||||
PhotoWidth int `json:"photo_width,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -126,6 +127,7 @@ type InlineQueryResultGif struct {
|
||||
GifDuration int `json:"gif_duration,omitempty"`
|
||||
GifHeight int `json:"gif_height,omitempty"`
|
||||
GifWidth int `json:"gif_width,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -149,6 +151,7 @@ type InlineQueryResultMpeg4Gif struct {
|
||||
Mpeg4Duration int `json:"mpeg4_duration,omitempty"`
|
||||
Mpeg4Height int `json:"mpeg4_height,omitempty"`
|
||||
Mpeg4Width int `json:"mpeg4_width,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -173,6 +176,7 @@ type InlineQueryResultVideo struct {
|
||||
VideoHeight int `json:"video_height,omitempty"`
|
||||
VideoDuration int `json:"video_duration,omitempty"`
|
||||
VideoWidth int `json:"video_width,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -333,6 +337,7 @@ type InlineQueryResultCachedPhoto struct {
|
||||
PhotoFileID string `json:"photo_file_id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -351,6 +356,7 @@ type InlineQueryResultCachedGif struct {
|
||||
ID string `json:"id"`
|
||||
GifFileID string `json:"gif_file_id"`
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -369,6 +375,7 @@ type InlineQueryResultCachedMpeg4Gif struct {
|
||||
ID string `json:"id"`
|
||||
Mpeg4FileID string `json:"mpeg4_file_id"`
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -421,6 +428,7 @@ type InlineQueryResultCachedVideo struct {
|
||||
VideoFileID string `json:"video_file_id"`
|
||||
Title string `json:"title"`
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
|
||||
@@ -468,10 +476,10 @@ type InputMessageContent interface {
|
||||
|
||||
// InputTextMessageContent represents the content of a text message to be sent as the result of an inline query.
|
||||
type InputTextMessageContent struct {
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
MessageText string `json:"message_text"`
|
||||
ParseMode string `json:"parse_mode,omitempty"`
|
||||
Entities []*MessageEntity `json:"entities,omitempty"`
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||
@@ -516,6 +524,33 @@ type InputContactMessageContent struct {
|
||||
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||
func (i InputContactMessageContent) ImplementsInputMessageContent() {}
|
||||
|
||||
// InputInvoiceMessageContent represents the content of an invoice message to be sent as the result of an inline query.
|
||||
type InputInvoiceMessageContent struct {
|
||||
SuggestedTipAmounts *[]int `json:"suggested_tip_amounts,omitempty"`
|
||||
PhotoURL string `json:"photo_url,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Payload string `json:"string"`
|
||||
ProviderToken string `json:"provider_token,omitempty"`
|
||||
Currency string `json:"currency"`
|
||||
Title string `json:"title"`
|
||||
ProviderData string `json:"provider_data,omitempty"`
|
||||
Prices []LabeledPrice `json:"prices"`
|
||||
PhotoSize int `json:"photo_size,omitempty"`
|
||||
MaxTipAmount int `json:"max_tip_amount,omitempty"`
|
||||
PhotoWidth int `json:"photo_width,omitempty"`
|
||||
PhotoHeight int `json:"photo_height,omitempty"`
|
||||
NeedName bool `json:"need_name,omitempty"`
|
||||
NeedPhoneNumber bool `json:"need_phone_number,omitempty"`
|
||||
NeedEmail bool `json:"need_email,omitempty"`
|
||||
NeedShippingAddress bool `json:"need_shipping_address,omitempty"`
|
||||
SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"`
|
||||
SendEmailToProvider bool `json:"send_email_to_provider,omitempty"`
|
||||
IsFlexible bool `json:"is_flexible,omitempty"`
|
||||
}
|
||||
|
||||
// ImplementsInputMessageContent is used to implement the InputMessageContent interface.
|
||||
func (i InputInvoiceMessageContent) ImplementsInputMessageContent() {}
|
||||
|
||||
// InlineQueryResultsButton represents a button to be shown above inline query results.
|
||||
// You MUST use exactly one of the fields.
|
||||
type InlineQueryResultsButton struct {
|
||||
@@ -539,5 +574,5 @@ func (a API) AnswerInlineQuery(inlineQueryID string, results []InlineQueryResult
|
||||
jsn, _ := json.Marshal(results)
|
||||
vals.Set("inline_query_id", inlineQueryID)
|
||||
vals.Set("results", string(jsn))
|
||||
return get[APIResponseBase](a.base, "answerInlineQuery", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "answerInlineQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
8
vendor/github.com/NicoNex/echotron/v3/menubutton.go
generated
vendored
8
vendor/github.com/NicoNex/echotron/v3/menubutton.go
generated
vendored
@@ -47,11 +47,11 @@ type GetChatMenuButtonOptions struct {
|
||||
}
|
||||
|
||||
// SetChatMenuButton is used to change the bot's menu button in a private chat, or the default menu button.
|
||||
func (a API) SetChatMenuButton(opts SetChatMenuButtonOptions) (res APIResponseBool, err error) {
|
||||
return get[APIResponseBool](a.base, "setChatMenuButton", urlValues(opts))
|
||||
func (a API) SetChatMenuButton(opts *SetChatMenuButtonOptions) (res APIResponseBool, err error) {
|
||||
return res, a.client.get(a.base, "setChatMenuButton", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// GetChatMenuButton is used to get the current value of the bot's menu button in a private chat, or the default menu button.
|
||||
func (a API) GetChatMenuButton(opts GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
|
||||
return get[APIResponseMenuButton](a.base, "getChatMenuButton", urlValues(opts))
|
||||
func (a API) GetChatMenuButton(opts *GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
|
||||
return res, a.client.get(a.base, "getChatMenuButton", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
338
vendor/github.com/NicoNex/echotron/v3/network.go
generated
vendored
338
vendor/github.com/NicoNex/echotron/v3/network.go
generated
vendored
@@ -20,99 +20,345 @@ package echotron
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// content is a struct which contains a file's name, its type and its data.
|
||||
type content struct {
|
||||
fname string
|
||||
ftype string
|
||||
fdata []byte
|
||||
type client struct {
|
||||
*http.Client
|
||||
*sync.RWMutex
|
||||
cl map[string]*rate.Limiter // chat based limiter
|
||||
gl *rate.Limiter // global limiter
|
||||
climiter func() *rate.Limiter
|
||||
}
|
||||
|
||||
// sendGetRequest is used to send an HTTP GET request.
|
||||
func sendGetRequest(url string) ([]byte, error) {
|
||||
resp, err := http.Get(url)
|
||||
var lclient = newClient()
|
||||
|
||||
// SetGlobalRequestLimit sets the global rate limit for requests to the Telegram API.
|
||||
// A duration of 0 disables the rate limiter, allowing unlimited requests.
|
||||
// By default the duration of this limiter is set to time.Second/30.
|
||||
func SetGlobalRequestLimit(d time.Duration) {
|
||||
lclient.Lock()
|
||||
lclient.gl = rate.NewLimiter(rate.Every(d), 10)
|
||||
lclient.Unlock()
|
||||
}
|
||||
|
||||
// SetChatRequestLimit sets the per-chat rate limit for requests to the Telegram API.
|
||||
// A duration of 0 disables the rate limiter, allowing unlimited requests.
|
||||
// By default the duration of this limiter is set to time.Minute/20.
|
||||
func SetChatRequestLimit(d time.Duration) {
|
||||
lclient.Lock()
|
||||
lclient.cl = make(map[string]*rate.Limiter)
|
||||
lclient.climiter = func() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Every(d), 1)
|
||||
}
|
||||
lclient.Unlock()
|
||||
}
|
||||
|
||||
func newClient() *client {
|
||||
return &client{
|
||||
Client: new(http.Client),
|
||||
RWMutex: new(sync.RWMutex),
|
||||
cl: make(map[string]*rate.Limiter),
|
||||
gl: rate.NewLimiter(rate.Every(time.Second/30), 10),
|
||||
climiter: func() *rate.Limiter {
|
||||
return rate.NewLimiter(rate.Every(time.Minute/20), 1)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c client) wait(chatID string) error {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
|
||||
ctx := context.Background()
|
||||
// If the chatID is empty, it's a general API call like GetUpdates, GetMe
|
||||
// and similar, so skip the per-chat request limit wait.
|
||||
if chatID != "" {
|
||||
// If no limiter exists for a chat, create one.
|
||||
l, ok := c.cl[chatID]
|
||||
if !ok {
|
||||
l = c.climiter()
|
||||
c.cl[chatID] = l
|
||||
}
|
||||
|
||||
// Make sure to respect the single chat limit of requests.
|
||||
if err := l.Wait(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to respect the global limit of requests.
|
||||
return c.gl.Wait(ctx)
|
||||
}
|
||||
|
||||
func (c client) doGet(reqURL string) ([]byte, error) {
|
||||
resp, err := c.Get(reqURL)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// sendPostRequest is used to send an HTTP POST request.
|
||||
func sendPostRequest(url string, files ...content) ([]byte, error) {
|
||||
var buf = new(bytes.Buffer)
|
||||
var w = multipart.NewWriter(buf)
|
||||
func (c client) doPost(reqURL string, files ...content) ([]byte, error) {
|
||||
var (
|
||||
buf = new(bytes.Buffer)
|
||||
w = multipart.NewWriter(buf)
|
||||
)
|
||||
|
||||
for _, f := range files {
|
||||
part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname))
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
part.Write(f.fdata)
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", url, buf)
|
||||
req, err := http.NewRequest(http.MethodPost, reqURL, buf)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Add("Content-Type", w.FormDataContentType())
|
||||
|
||||
var client = new(http.Client)
|
||||
res, err := client.Do(req)
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
cnt, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
return cnt, nil
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
// sendPostForm is used to send an "application/x-www-form-urlencoded" through an HTTP POST request.
|
||||
func sendPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
|
||||
func (c client) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
|
||||
var form = make(url.Values)
|
||||
|
||||
for k, v := range keyVals {
|
||||
form.Add(k, v)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", reqURL, strings.NewReader(form.Encode()))
|
||||
req, err := http.NewRequest(http.MethodPost, reqURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
request.PostForm = form
|
||||
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.PostForm = form
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
var client http.Client
|
||||
|
||||
response, err := client.Do(request)
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
return content, nil
|
||||
defer res.Body.Close()
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
func (c client) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
|
||||
var cnt []content
|
||||
|
||||
if file.id != "" {
|
||||
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.id)
|
||||
} else if file.url != "" {
|
||||
url = fmt.Sprintf("%s&%s=%s", url, fileType, file.url)
|
||||
} else if c, e := toContent(fileType, file); e == nil {
|
||||
cnt = append(cnt, c)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
if c, e := toContent("thumbnail", thumbnail); e == nil {
|
||||
cnt = append(cnt, c)
|
||||
} else {
|
||||
err = e
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
res, err = c.doPost(url, cnt...)
|
||||
} else {
|
||||
res, err = c.doGet(url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c client) get(base, endpoint string, vals url.Values, v APIResponse) error {
|
||||
url, err := url.JoinPath(base, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if vals != nil {
|
||||
if queries := vals.Encode(); queries != "" {
|
||||
url = fmt.Sprintf("%s?%s", url, queries)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.doGet(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
}
|
||||
|
||||
func (c client) postFile(base, endpoint, fileType string, file, thumbnail InputFile, vals url.Values, v APIResponse) error {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.sendFile(file, thumbnail, url, fileType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
}
|
||||
|
||||
func (c client) postMedia(base, endpoint string, editSingle bool, vals url.Values, v APIResponse, files ...InputMedia) error {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.sendMediaFiles(url, editSingle, files...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
}
|
||||
|
||||
func (c client) postStickers(base, endpoint string, vals url.Values, v APIResponse, stickers ...InputSticker) error {
|
||||
url, err := joinURL(base, endpoint, vals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.wait(vals.Get("chat_id")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := c.sendStickers(url, stickers...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(cnt, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return check(v)
|
||||
}
|
||||
|
||||
func (c client) sendMediaFiles(url string, editSingle bool, files ...InputMedia) (res []byte, err error) {
|
||||
var (
|
||||
med []mediaEnvelope
|
||||
cnt []content
|
||||
jsn []byte
|
||||
)
|
||||
|
||||
for _, file := range files {
|
||||
var im mediaEnvelope
|
||||
var cntArr []content
|
||||
|
||||
media := file.media()
|
||||
thumbnail := file.thumbnail()
|
||||
|
||||
im, cntArr, err = processMedia(media, thumbnail)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
im.InputMedia = file
|
||||
|
||||
med = append(med, im)
|
||||
cnt = append(cnt, cntArr...)
|
||||
}
|
||||
|
||||
if editSingle {
|
||||
jsn, err = json.Marshal(med[0])
|
||||
} else {
|
||||
jsn, err = json.Marshal(med)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url = fmt.Sprintf("%s&media=%s", url, jsn)
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return c.doPost(url, cnt...)
|
||||
}
|
||||
return c.doGet(url)
|
||||
}
|
||||
|
||||
func (c client) sendStickers(url string, stickers ...InputSticker) (res []byte, err error) {
|
||||
var (
|
||||
sti []stickerEnvelope
|
||||
cnt []content
|
||||
jsn []byte
|
||||
)
|
||||
|
||||
for _, s := range stickers {
|
||||
var se stickerEnvelope
|
||||
var cntArr []content
|
||||
|
||||
se, cntArr, err = processSticker(s.Sticker)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
se.InputSticker = s
|
||||
|
||||
sti = append(sti, se)
|
||||
cnt = append(cnt, cntArr...)
|
||||
}
|
||||
|
||||
if len(sti) == 1 {
|
||||
jsn, _ = json.Marshal(sti[0])
|
||||
url = fmt.Sprintf("%s&sticker=%s", url, jsn)
|
||||
} else {
|
||||
jsn, _ = json.Marshal(sti)
|
||||
url = fmt.Sprintf("%s&stickers=%s", url, jsn)
|
||||
}
|
||||
|
||||
if len(cnt) > 0 {
|
||||
return c.doPost(url, cnt...)
|
||||
}
|
||||
return c.doGet(url)
|
||||
}
|
||||
|
||||
118
vendor/github.com/NicoNex/echotron/v3/options.go
generated
vendored
118
vendor/github.com/NicoNex/echotron/v3/options.go
generated
vendored
@@ -86,6 +86,8 @@ const (
|
||||
UnderlineEntity = "underline"
|
||||
StrikethroughEntity = "strikethrough"
|
||||
SpoilerEntity = "spoiler"
|
||||
BlockQuoteEntity = "blockquote"
|
||||
ExpandableBlockQuoteEntity = "expandable_blockquote"
|
||||
CodeEntity = "code"
|
||||
PreEntity = "pre"
|
||||
TextLinkEntity = "text_link"
|
||||
@@ -141,6 +143,9 @@ type KeyboardButtonRequestUsers struct {
|
||||
MaxQuantity int `json:"max_quantity,omitempty"`
|
||||
UserIsBot bool `json:"user_is_bot,omitempty"`
|
||||
UserIsPremium bool `json:"user_is_premium,omitempty"`
|
||||
RequestName bool `json:"request_name,omitempty"`
|
||||
RequestUsername bool `json:"request_username,omitempty"`
|
||||
RequestPhoto bool `json:"request_photo,omitempty"`
|
||||
}
|
||||
|
||||
// KeyboardButtonRequestChat defines the criteria used to request a suitable chat.
|
||||
@@ -154,6 +159,9 @@ type KeyboardButtonRequestChat struct {
|
||||
ChatHasUsername bool `json:"chat_has_username,omitempty"`
|
||||
ChatIsCreated bool `json:"chat_is_created,omitempty"`
|
||||
BotIsMember bool `json:"bot_is_member,omitempty"`
|
||||
RequestName bool `json:"request_name,omitempty"`
|
||||
RequestUsername bool `json:"request_username,omitempty"`
|
||||
RequestPhoto bool `json:"request_photo,omitempty"`
|
||||
}
|
||||
|
||||
// ReplyKeyboardMarkup represents a custom keyboard with reply options.
|
||||
@@ -233,6 +241,8 @@ type WebhookOptions struct {
|
||||
|
||||
// BaseOptions contains the optional parameters used frequently in some Telegram API methods.
|
||||
type BaseOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
@@ -243,10 +253,12 @@ type BaseOptions struct {
|
||||
// MessageOptions contains the optional parameters used by some Telegram API methods.
|
||||
type MessageOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
|
||||
Entities []MessageEntity `query:"entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int64 `query:"message_thread_id"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
@@ -267,13 +279,14 @@ type ForwardOptions struct {
|
||||
// CopyOptions contains the optional parameters used by the CopyMessage method.
|
||||
type CopyOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Caption string `query:"caption"`
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||
}
|
||||
|
||||
// CopyMessagesOptions contains the optional parameters used by the CopyMessages methods.
|
||||
@@ -286,7 +299,9 @@ type CopyMessagesOptions struct {
|
||||
|
||||
// StickerOptions contains the optional parameters used by the SendSticker method.
|
||||
type StickerOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
Emoji string `query:"emoji"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
@@ -299,7 +314,6 @@ type InputFile struct {
|
||||
id string
|
||||
path string
|
||||
url string
|
||||
ftype string
|
||||
content []byte
|
||||
}
|
||||
|
||||
@@ -326,26 +340,31 @@ func NewInputFileBytes(fileName string, content []byte) InputFile {
|
||||
// PhotoOptions contains the optional parameters used by the SendPhoto method.
|
||||
type PhotoOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Caption string `query:"caption"`
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
HasSpoiler bool `query:"has_spoiler"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||
}
|
||||
|
||||
// AudioOptions contains the optional parameters used by the SendAudio method.
|
||||
type AudioOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
Title string `query:"title"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Caption string `query:"caption"`
|
||||
Performer string `query:"performer"`
|
||||
Title string `query:"title"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
Thumbnail InputFile
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
Duration int `query:"duration"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
@@ -355,11 +374,13 @@ type AudioOptions struct {
|
||||
// DocumentOptions contains the optional parameters used by the SendDocument method.
|
||||
type DocumentOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Caption string `query:"caption"`
|
||||
Thumbnail InputFile
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
@@ -369,11 +390,13 @@ type DocumentOptions struct {
|
||||
// VideoOptions contains the optional parameters used by the SendVideo method.
|
||||
type VideoOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
Caption string `query:"caption"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Thumbnail InputFile
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
Duration int `query:"duration"`
|
||||
Width int `query:"width"`
|
||||
@@ -382,16 +405,19 @@ type VideoOptions struct {
|
||||
SupportsStreaming bool `query:"supports_streaming"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||
}
|
||||
|
||||
// AnimationOptions contains the optional parameters used by the SendAnimation method.
|
||||
type AnimationOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Caption string `query:"caption"`
|
||||
Thumbnail InputFile
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
Duration int `query:"duration"`
|
||||
Width int `query:"width"`
|
||||
@@ -399,15 +425,18 @@ type AnimationOptions struct {
|
||||
HasSpoiler bool `query:"has_spoiler"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||
}
|
||||
|
||||
// VoiceOptions contains the optional parameters used by the SendVoice method.
|
||||
type VoiceOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
Caption string `query:"caption"`
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
Duration int `query:"duration"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
@@ -417,8 +446,10 @@ type VoiceOptions struct {
|
||||
// VideoNoteOptions contains the optional parameters used by the SendVideoNote method.
|
||||
type VideoNoteOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
Thumbnail InputFile
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
Duration int `query:"duration"`
|
||||
Length int `query:"length"`
|
||||
@@ -428,14 +459,21 @@ type VideoNoteOptions struct {
|
||||
|
||||
// MediaGroupOptions contains the optional parameters used by the SendMediaGroup method.
|
||||
type MediaGroupOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
}
|
||||
|
||||
// This is a custom constant to set an infinite live period value in LocationOptions and EditLocationOptions.
|
||||
const InfiniteLivePeriod = 0x7FFFFFFF
|
||||
|
||||
// LocationOptions contains the optional parameters used by the SendLocation method.
|
||||
type LocationOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
HorizontalAccuracy float64 `query:"horizontal_accuracy"`
|
||||
@@ -449,20 +487,30 @@ type LocationOptions struct {
|
||||
|
||||
// EditLocationOptions contains the optional parameters used by the EditMessageLiveLocation method.
|
||||
type EditLocationOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||
HorizontalAccuracy float64 `query:"horizontal_accuracy"`
|
||||
Heading int `query:"heading"`
|
||||
LivePeriod int `query:"live_period"`
|
||||
ProximityAlertRadius int `query:"proximity_alert_radius"`
|
||||
}
|
||||
|
||||
// StopLocationOptions contains the optional parameters used by the StopMessageLiveLocation method.
|
||||
type StopLocationOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
}
|
||||
|
||||
// VenueOptions contains the optional parameters used by the SendVenue method.
|
||||
type VenueOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
FoursquareID string `query:"foursquare_id"`
|
||||
FoursquareType string `query:"foursquare_type"`
|
||||
GooglePlaceType string `query:"google_place_type"`
|
||||
GooglePlaceID string `query:"google_place_id"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
@@ -471,9 +519,11 @@ type VenueOptions struct {
|
||||
// ContactOptions contains the optional parameters used by the SendContact method.
|
||||
type ContactOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
VCard string `query:"vcard"`
|
||||
LastName string `query:"last_name"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
ProtectContent bool `query:"protect_content"`
|
||||
@@ -482,13 +532,17 @@ type ContactOptions struct {
|
||||
// PollOptions contains the optional parameters used by the SendPoll method.
|
||||
type PollOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
Explanation string `query:"explanation"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ExplanationParseMode ParseMode `query:"explanation_parse_mode"`
|
||||
QuestionParseMode ParseMode `query:"question_parse_mode"`
|
||||
Type PollType `query:"type"`
|
||||
ExplanationEntities []MessageEntity `query:"explanation_entities"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
QuestionEntities []MessageEntity `query:"question_entities"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
CorrectOptionID int `query:"correct_option_id"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
CloseDate int `query:"close_date"`
|
||||
OpenPeriod int `query:"open_period"`
|
||||
IsClosed bool `query:"is_closed"`
|
||||
@@ -498,6 +552,12 @@ type PollOptions struct {
|
||||
IsAnonymous bool `query:"is_anonymous"`
|
||||
}
|
||||
|
||||
// StopPollOptions contains the optional parameters used by the StopPoll method.
|
||||
type StopPollOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
}
|
||||
|
||||
// BanOptions contains the optional parameters used by the BanChatMember method.
|
||||
type BanOptions struct {
|
||||
UntilDate int `query:"until_date"`
|
||||
@@ -581,6 +641,7 @@ func NewInlineMessageID(ID string) MessageIDOptions {
|
||||
// MessageTextOptions contains the optional parameters used by the EditMessageText method.
|
||||
type MessageTextOptions struct {
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
Entities []MessageEntity `query:"entities"`
|
||||
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
|
||||
@@ -589,13 +650,22 @@ type MessageTextOptions struct {
|
||||
// MessageCaptionOptions contains the optional parameters used by the EditMessageCaption method.
|
||||
type MessageCaptionOptions struct {
|
||||
Caption string `query:"caption"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
ParseMode ParseMode `query:"parse_mode"`
|
||||
CaptionEntities []MessageEntity `query:"caption_entities"`
|
||||
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
|
||||
}
|
||||
|
||||
// MessageReplyMarkup contains the optional parameters used by the method which only require reply_markup.
|
||||
type MessageReplyMarkup struct {
|
||||
// MessageMediaOptions contains the optional parameters used by the EditMessageMedia method.
|
||||
type MessageMediaOptions struct {
|
||||
ReplyMarkup ReplyMarkup `query:"reply_markup"`
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
}
|
||||
|
||||
// MessageReplyMarkupOptions contains the optional parameters used by the EditMessageReplyMarkup method.
|
||||
type MessageReplyMarkupOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||
}
|
||||
|
||||
@@ -610,17 +680,19 @@ type InvoiceOptions struct {
|
||||
StartParameter string `query:"start_parameter"`
|
||||
ProviderData string `query:"provider_data"`
|
||||
PhotoURL string `query:"photo_url"`
|
||||
ProviderToken string `query:"provider_token"`
|
||||
MessageEffectID string `query:"message_effect_id"`
|
||||
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
SuggestedTipAmount []int `query:"suggested_tip_amounts"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
ReplyParameters ReplyParameters `query:"reply_parameters"`
|
||||
MaxTipAmount int `query:"max_tip_amount"`
|
||||
PhotoSize int `query:"photo_size"`
|
||||
PhotoWidth int `query:"photo_width"`
|
||||
PhotoHeight int `query:"photo_height"`
|
||||
MaxTipAmount int `query:"max_tip_amount"`
|
||||
NeedPhoneNumber bool `query:"need_phone_number"`
|
||||
NeepShippingAddress bool `query:"need_shipping_address"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"`
|
||||
NeepShippingAddress bool `query:"need_shipping_address"`
|
||||
NeedPhoneNumber bool `query:"need_phone_number"`
|
||||
SendEmailToProvider bool `query:"send_email_to_provider"`
|
||||
IsFlexible bool `query:"is_flexible"`
|
||||
DisableNotification bool `query:"disable_notification"`
|
||||
@@ -633,6 +705,7 @@ type InvoiceOptions struct {
|
||||
type CreateInvoiceLinkOptions struct {
|
||||
ProviderData string `query:"provider_data"`
|
||||
PhotoURL string `query:"photo_url"`
|
||||
ProviderToken string `query:"provider_token"`
|
||||
SuggestedTipAmounts []int `query:"suggested_tip_amounts"`
|
||||
PhotoSize int `query:"photo_size"`
|
||||
PhotoWidth int `query:"photo_width"`
|
||||
@@ -679,6 +752,7 @@ type EditTopicOptions struct {
|
||||
|
||||
// ChatActionOptions contains the optional parameters used by the SendChatAction API method.
|
||||
type ChatActionOptions struct {
|
||||
BusinessConnectionID string `query:"business_connection_id"`
|
||||
MessageThreadID int `query:"message_thread_id"`
|
||||
}
|
||||
|
||||
|
||||
2
vendor/github.com/NicoNex/echotron/v3/passport.go
generated
vendored
2
vendor/github.com/NicoNex/echotron/v3/passport.go
generated
vendored
@@ -224,5 +224,5 @@ func (a API) SetPassportDataErrors(userID int64, errors []PassportElementError)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("errors", string(errorsArr))
|
||||
return get[APIResponseBool](a.base, "setPassportDataErrors", vals)
|
||||
return res, a.client.get(a.base, "setPassportDataErrors", vals, &res)
|
||||
}
|
||||
|
||||
132
vendor/github.com/NicoNex/echotron/v3/payments.go
generated
vendored
132
vendor/github.com/NicoNex/echotron/v3/payments.go
generated
vendored
@@ -96,8 +96,90 @@ type PreCheckoutQuery struct {
|
||||
TotalAmount int `json:"total_amount"`
|
||||
}
|
||||
|
||||
// RevenueWithdrawalState describes the state of a revenue withdrawal operation.
|
||||
type RevenueWithdrawalState interface {
|
||||
ImplementsRevenueWithdrawalState()
|
||||
}
|
||||
|
||||
// RevenueWithdrawalStatePending describes the state of a withdrawal in progress.
|
||||
type RevenueWithdrawalStatePending struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
|
||||
func (r RevenueWithdrawalStatePending) ImplementsRevenueWithdrawalState() {}
|
||||
|
||||
// RevenueWithdrawalStateSucceeded describes the state of a succeeded withdrawal.
|
||||
type RevenueWithdrawalStateSucceeded struct {
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Date int `json:"date"`
|
||||
}
|
||||
|
||||
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
|
||||
func (r RevenueWithdrawalStateSucceeded) ImplementsRevenueWithdrawalState() {}
|
||||
|
||||
// RevenueWithdrawalStateFailed describes the state of a failed withdrawal, in which the transaction was refunded.
|
||||
type RevenueWithdrawalStateFailed struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ImplementsRevenueWithdrawalState is used to implement the RevenueWithdrawalState interface.
|
||||
func (r RevenueWithdrawalStateFailed) ImplementsRevenueWithdrawalState() {}
|
||||
|
||||
// TransactionPartner describes the source of a transaction, or its recipient for outgoing transactions.
|
||||
type TransactionPartner interface {
|
||||
ImplementsTransactionPartner()
|
||||
}
|
||||
|
||||
// TransactionPartnerFragment describes a withdrawal transaction with Fragment.
|
||||
type TransactionPartnerFragment struct {
|
||||
WithdrawalState RevenueWithdrawalState `json:"withdrawal_state"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||
func (t TransactionPartnerFragment) ImplementsTransactionPartner() {}
|
||||
|
||||
// TransactionPartnerUser describes a transaction with a user.
|
||||
type TransactionPartnerUser struct {
|
||||
Type string `json:"type"`
|
||||
User User `json:"user"`
|
||||
}
|
||||
|
||||
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||
func (t TransactionPartnerUser) ImplementsTransactionPartner() {}
|
||||
|
||||
// TransactionPartnerOther describes a transaction with an unknown source or recipient.
|
||||
type TransactionPartnerOther struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ImplementsTransactionPartner is used to implement the TransactionPartner interface.
|
||||
func (t TransactionPartnerOther) ImplementsTransactionPartner() {}
|
||||
|
||||
// StarTransaction describes a Telegram Star transaction.
|
||||
type StarTransaction struct {
|
||||
Source TransactionPartner `json:"source"`
|
||||
Receiver TransactionPartner `json:"receiver"`
|
||||
ID string `json:"id"`
|
||||
Amount int `json:"amount"`
|
||||
Date int `json:"date"`
|
||||
}
|
||||
|
||||
// StarTransactions contains a list of Telegram Star transactions.
|
||||
type StarTransactions struct {
|
||||
Transaction []StarTransaction `json:"transaction"`
|
||||
}
|
||||
|
||||
// StarTransactionsOptions contains the optional parameters used by the GetStarTransactions method.
|
||||
type StarTransactionsOptions struct {
|
||||
Offset int `query:"offset"`
|
||||
Limit int `query:"limit"`
|
||||
}
|
||||
|
||||
// SendInvoice is used to send invoices.
|
||||
func (a API) SendInvoice(chatID int64, title, description, payload, providerToken, currency string, prices []LabeledPrice, opts *InvoiceOptions) (res APIResponseMessage, err error) {
|
||||
func (a API) SendInvoice(chatID int64, title, description, payload, currency string, prices []LabeledPrice, opts *InvoiceOptions) (res APIResponseMessage, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
p, err := json.Marshal(prices)
|
||||
@@ -109,10 +191,26 @@ func (a API) SendInvoice(chatID int64, title, description, payload, providerToke
|
||||
vals.Set("title", title)
|
||||
vals.Set("description", description)
|
||||
vals.Set("payload", payload)
|
||||
vals.Set("provider_token", providerToken)
|
||||
vals.Set("currency", currency)
|
||||
vals.Set("prices", string(p))
|
||||
return get[APIResponseMessage](a.base, "sendInvoice", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendInvoice", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// CreateInvoiceLink creates a link for an invoice.
|
||||
func (a API) CreateInvoiceLink(title, description, payload, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
p, err := json.Marshal(prices)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
vals.Set("title", title)
|
||||
vals.Set("description", description)
|
||||
vals.Set("payload", payload)
|
||||
vals.Set("currency", currency)
|
||||
vals.Set("prices", string(p))
|
||||
return res, a.client.get(a.base, "createInvoiceLink", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// AnswerShippingQuery is used to reply to shipping queries.
|
||||
@@ -123,7 +221,7 @@ func (a API) AnswerShippingQuery(shippingQueryID string, ok bool, opts *Shipping
|
||||
|
||||
vals.Set("shipping_query_id", shippingQueryID)
|
||||
vals.Set("ok", btoa(ok))
|
||||
return get[APIResponseBase](a.base, "answerShippingQuery", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "answerShippingQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// AnswerPreCheckoutQuery is used to respond to such pre-checkout queries.
|
||||
@@ -135,23 +233,19 @@ func (a API) AnswerPreCheckoutQuery(preCheckoutQueryID string, ok bool, opts *Pr
|
||||
|
||||
vals.Set("pre_checkout_query_id", preCheckoutQueryID)
|
||||
vals.Set("ok", btoa(ok))
|
||||
return get[APIResponseBase](a.base, "answerPreCheckoutQuery", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "answerPreCheckoutQuery", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// CreateInvoiceLink creates a link for an invoice.
|
||||
func (a API) CreateInvoiceLink(title, description, payload, providerToken, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) {
|
||||
// GetStarTransactions returns the bot's Telegram Star transactions in chronological order.
|
||||
func (a API) GetStarTransactions(opts *StarTransactionsOptions) (res APIResponseStarTransactions, err error) {
|
||||
return res, a.client.get(a.base, "getStarTransactions", urlValues(opts), &res)
|
||||
}
|
||||
|
||||
// RefundStarPayment refunds a successful payment in Telegram Stars.
|
||||
func (a API) RefundStarPayment(userID int64, telegramPaymentChargeID string) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
p, err := json.Marshal(prices)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
vals.Set("title", title)
|
||||
vals.Set("description", description)
|
||||
vals.Set("payload", payload)
|
||||
vals.Set("provider_token", providerToken)
|
||||
vals.Set("currency", currency)
|
||||
vals.Set("prices", string(p))
|
||||
return get[APIResponseBase](a.base, "createInvoiceLink", addValues(vals, opts))
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
|
||||
return res, a.client.get(a.base, "refundStarPayment", vals, &res)
|
||||
}
|
||||
|
||||
4
vendor/github.com/NicoNex/echotron/v3/querybuilder.go
generated
vendored
4
vendor/github.com/NicoNex/echotron/v3/querybuilder.go
generated
vendored
@@ -70,10 +70,6 @@ func scan(i any, v url.Values) url.Values {
|
||||
return v
|
||||
}
|
||||
|
||||
func querify(i any) string {
|
||||
return scan(i, url.Values{}).Encode()
|
||||
}
|
||||
|
||||
func urlValues(i any) url.Values {
|
||||
if i == nil {
|
||||
return nil
|
||||
|
||||
79
vendor/github.com/NicoNex/echotron/v3/stickers.go
generated
vendored
79
vendor/github.com/NicoNex/echotron/v3/stickers.go
generated
vendored
@@ -20,7 +20,6 @@ package echotron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
@@ -50,8 +49,6 @@ type StickerSet struct {
|
||||
Name string `json:"name"`
|
||||
StickerType StickerSetType `json:"sticker_type"`
|
||||
Stickers []Sticker `json:"stickers"`
|
||||
IsAnimated bool `json:"is_animated"`
|
||||
IsVideo bool `json:"is_video"`
|
||||
}
|
||||
|
||||
// StickerSetType represents the type of a sticker or of the entire set
|
||||
@@ -100,16 +97,17 @@ type NewStickerSetOptions struct {
|
||||
|
||||
// InputSticker is a struct which describes a sticker to be added to a sticker set.
|
||||
type InputSticker struct {
|
||||
Sticker InputFile `json:"-"`
|
||||
EmojiList []string `json:"emoji_list"`
|
||||
MaskPosition *MaskPosition `json:"mask_position,omitempty"`
|
||||
Keywords *[]string `json:"keywords,omitempty"`
|
||||
Format StickerFormat `json:"format"`
|
||||
Sticker InputFile `json:"-"`
|
||||
EmojiList []string `json:"emoji_list"`
|
||||
}
|
||||
|
||||
// stickerEnvelope is a generic struct for all the various structs under the InputSticker interface.
|
||||
type stickerEnvelope struct {
|
||||
InputSticker
|
||||
Sticker string `json:"sticker"`
|
||||
InputSticker
|
||||
}
|
||||
|
||||
// SendSticker is used to send static .WEBP or animated .TGS stickers.
|
||||
@@ -118,7 +116,7 @@ func (a API) SendSticker(stickerID string, chatID int64, opts *StickerOptions) (
|
||||
|
||||
vals.Set("sticker", stickerID)
|
||||
vals.Set("chat_id", itoa(chatID))
|
||||
return get[APIResponseMessage](a.base, "sendSticker", addValues(vals, opts))
|
||||
return res, a.client.get(a.base, "sendSticker", addValues(vals, opts), &res)
|
||||
}
|
||||
|
||||
// GetStickerSet is used to get a sticker set.
|
||||
@@ -126,30 +124,16 @@ func (a API) GetStickerSet(name string) (res APIResponseStickerSet, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("name", name)
|
||||
return get[APIResponseStickerSet](a.base, "getStickerSet", vals)
|
||||
return res, a.client.get(a.base, "getStickerSet", vals, &res)
|
||||
}
|
||||
|
||||
// GetCustomEmojiStickers is used to get information about custom emoji stickers by their identifiers.
|
||||
func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseStickers, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
jsn, _ := json.Marshal(customEmojiIDs)
|
||||
|
||||
var url = fmt.Sprintf(
|
||||
"%sgetCustomEmojiStickers?custom_emoji_ids=%s",
|
||||
a.base,
|
||||
jsn,
|
||||
)
|
||||
|
||||
cnt, err := sendGetRequest(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(cnt, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = check(res)
|
||||
return
|
||||
vals.Set("custom_emoji_ids", string(jsn))
|
||||
return res, a.client.get(a.base, "getCustomEmojiStickers", vals, &res)
|
||||
}
|
||||
|
||||
// UploadStickerFile is used to upload a .PNG file with a sticker for later use in
|
||||
@@ -159,18 +143,17 @@ func (a API) UploadStickerFile(userID int64, sticker InputFile, format StickerFo
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("sticker_format", string(format))
|
||||
return postFile[APIResponseFile](a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals)
|
||||
return res, a.client.postFile(a.base, "uploadStickerFile", "sticker", sticker, InputFile{}, vals, &res)
|
||||
}
|
||||
|
||||
// CreateNewStickerSet is used to create a new sticker set owned by a user.
|
||||
func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []InputSticker, format StickerFormat, opts *NewStickerSetOptions) (res APIResponseBool, err error) {
|
||||
func (a API) CreateNewStickerSet(userID int64, name, title string, stickers []InputSticker, opts *NewStickerSetOptions) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("name", name)
|
||||
vals.Set("title", title)
|
||||
vals.Set("sticker_format", string(format))
|
||||
return postStickers[APIResponseBool](a.base, "createNewStickerSet", addValues(vals, opts), stickers...)
|
||||
return res, a.client.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
|
||||
}
|
||||
|
||||
// AddStickerToSet is used to add a new sticker to a set created by the bot.
|
||||
@@ -179,7 +162,7 @@ func (a API) AddStickerToSet(userID int64, name string, sticker InputSticker) (r
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("name", name)
|
||||
return postStickers[APIResponseBool](a.base, "addStickerToSet", vals, sticker)
|
||||
return res, a.client.postStickers(a.base, "addStickerToSet", vals, &res, sticker)
|
||||
}
|
||||
|
||||
// SetStickerPositionInSet is used to move a sticker in a set created by the bot to a specific position.
|
||||
@@ -188,7 +171,7 @@ func (a API) SetStickerPositionInSet(sticker string, position int) (res APIRespo
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("position", itoa(int64(position)))
|
||||
return get[APIResponseBase](a.base, "setStickerPositionInSet", vals)
|
||||
return res, a.client.get(a.base, "setStickerPositionInSet", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteStickerFromSet is used to delete a sticker from a set created by the bot.
|
||||
@@ -196,7 +179,18 @@ func (a API) DeleteStickerFromSet(sticker string) (res APIResponseBase, err erro
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
return get[APIResponseBase](a.base, "deleteStickerFromSet", vals)
|
||||
return res, a.client.get(a.base, "deleteStickerFromSet", vals, &res)
|
||||
}
|
||||
|
||||
// ReplaceStickerInSet is used to replace an existing sticker in a sticker set with a new one.
|
||||
// The method is equivalent to calling DeleteStickerFromSet, then AddStickerToSet, then SetStickerPositionInSet.
|
||||
func (a API) ReplaceStickerInSet(userID int64, name string, old_sticker string, sticker InputSticker) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("user_id", itoa(userID))
|
||||
vals.Set("name", name)
|
||||
vals.Set("old_sticker", old_sticker)
|
||||
return res, a.client.postStickers(a.base, "replaceStickerInSet", vals, &res, sticker)
|
||||
}
|
||||
|
||||
// SetStickerEmojiList is used to change the list of emoji assigned to a regular or custom emoji sticker.
|
||||
@@ -208,7 +202,7 @@ func (a API) SetStickerEmojiList(sticker string, emojis []string) (res APIRespon
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("emoji_list", string(jsn))
|
||||
return get[APIResponseBool](a.base, "setStickerEmojiList", vals)
|
||||
return res, a.client.get(a.base, "setStickerEmojiList", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerKeywords is used to change search keywords assigned to a regular or custom emoji sticker.
|
||||
@@ -220,7 +214,7 @@ func (a API) SetStickerKeywords(sticker string, keywords []string) (res APIRespo
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("keywords", string(jsn))
|
||||
return get[APIResponseBool](a.base, "setStickerKeywords", vals)
|
||||
return res, a.client.get(a.base, "setStickerKeywords", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerMaskPosition is used to change the mask position of a mask sticker.
|
||||
@@ -232,7 +226,7 @@ func (a API) SetStickerMaskPosition(sticker string, mask MaskPosition) (res APIR
|
||||
|
||||
vals.Set("sticker", sticker)
|
||||
vals.Set("mask_position", string(jsn))
|
||||
return get[APIResponseBool](a.base, "setStickerMaskPosition", vals)
|
||||
return res, a.client.get(a.base, "setStickerMaskPosition", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerSetTitle is used to set the title of a created sticker set.
|
||||
@@ -241,16 +235,17 @@ func (a API) SetStickerSetTitle(name, title string) (res APIResponseBool, err er
|
||||
|
||||
vals.Set("name", name)
|
||||
vals.Set("title", title)
|
||||
return get[APIResponseBool](a.base, "setStickerSetTitle", vals)
|
||||
return res, a.client.get(a.base, "setStickerSetTitle", vals, &res)
|
||||
}
|
||||
|
||||
// SetStickerSetThumbnail is used to set the thumbnail of a sticker set.
|
||||
func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFile) (res APIResponseBase, err error) {
|
||||
func (a API) SetStickerSetThumbnail(name string, userID int64, thumbnail InputFile, format StickerFormat) (res APIResponseBase, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("name", name)
|
||||
vals.Set("user_id", itoa(userID))
|
||||
return postFile[APIResponseBase](a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals)
|
||||
vals.Set("format", string(format))
|
||||
return res, a.client.postFile(a.base, "setStickerSetThumbnail", "thumbnail", thumbnail, InputFile{}, vals, &res)
|
||||
}
|
||||
|
||||
// SetCustomEmojiStickerSetThumbnail is used to set the thumbnail of a custom emoji sticker set.
|
||||
@@ -259,7 +254,7 @@ func (a API) SetCustomEmojiStickerSetThumbnail(name, emojiID string) (res APIRes
|
||||
|
||||
vals.Set("name", name)
|
||||
vals.Set("custom_emoji_id", emojiID)
|
||||
return get[APIResponseBool](a.base, "setCustomEmojiStickerSetThumbnail", vals)
|
||||
return res, a.client.get(a.base, "setCustomEmojiStickerSetThumbnail", vals, &res)
|
||||
}
|
||||
|
||||
// DeleteStickerSet is used to delete a sticker set that was created by the bot.
|
||||
@@ -267,10 +262,10 @@ func (a API) DeleteStickerSet(name string) (res APIResponseBool, err error) {
|
||||
var vals = make(url.Values)
|
||||
|
||||
vals.Set("name", name)
|
||||
return get[APIResponseBool](a.base, "DeleteStickerSet", vals)
|
||||
return res, a.client.get(a.base, "DeleteStickerSet", vals, &res)
|
||||
}
|
||||
|
||||
// GetForumTopicIconStickers is used to get custom emoji stickers, which can be used as a forum topic icon by any user.
|
||||
func (a API) GetForumTopicIconStickers() (res APIResponseStickers, err error) {
|
||||
return get[APIResponseStickers](a.base, "getForumTopicIconStickers", nil)
|
||||
return res, a.client.get(a.base, "getForumTopicIconStickers", nil, &res)
|
||||
}
|
||||
|
||||
370
vendor/github.com/NicoNex/echotron/v3/types.go
generated
vendored
370
vendor/github.com/NicoNex/echotron/v3/types.go
generated
vendored
@@ -30,6 +30,10 @@ type Update struct {
|
||||
EditedMessage *Message `json:"edited_message,omitempty"`
|
||||
ChannelPost *Message `json:"channel_post,omitempty"`
|
||||
EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
|
||||
BusinessConnection *BusinessConnection `json:"business_connection,omitempty"`
|
||||
BusinessMessage *Message `json:"business_message,omitempty"`
|
||||
EditedBusinessMessage *Message `json:"edited_business_message,omitempty"`
|
||||
DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"`
|
||||
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
|
||||
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
|
||||
InlineQuery *InlineQuery `json:"inline_query,omitempty"`
|
||||
@@ -37,6 +41,8 @@ type Update struct {
|
||||
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
|
||||
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"`
|
||||
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"`
|
||||
Poll *Poll `json:"poll,omitempty"`
|
||||
PollAnswer *PollAnswer `json:"poll_answer,omitempty"`
|
||||
MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"`
|
||||
ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"`
|
||||
ID int `json:"update_id"`
|
||||
@@ -45,6 +51,12 @@ type Update struct {
|
||||
// ChatID returns the ID of the chat the update is coming from.
|
||||
func (u Update) ChatID() int64 {
|
||||
switch {
|
||||
case u.ChatJoinRequest != nil:
|
||||
return u.ChatJoinRequest.Chat.ID
|
||||
case u.ChatBoost != nil:
|
||||
return u.ChatBoost.Chat.ID
|
||||
case u.RemovedChatBoost != nil:
|
||||
return u.RemovedChatBoost.Chat.ID
|
||||
case u.Message != nil:
|
||||
return u.Message.Chat.ID
|
||||
case u.EditedMessage != nil:
|
||||
@@ -53,6 +65,18 @@ func (u Update) ChatID() int64 {
|
||||
return u.ChannelPost.Chat.ID
|
||||
case u.EditedChannelPost != nil:
|
||||
return u.EditedChannelPost.Chat.ID
|
||||
case u.BusinessConnection != nil:
|
||||
return u.BusinessConnection.User.ID
|
||||
case u.BusinessMessage != nil:
|
||||
return u.BusinessMessage.Chat.ID
|
||||
case u.EditedBusinessMessage != nil:
|
||||
return u.EditedBusinessMessage.Chat.ID
|
||||
case u.DeletedBusinessMessages != nil:
|
||||
return u.DeletedBusinessMessages.Chat.ID
|
||||
case u.MessageReaction != nil:
|
||||
return u.MessageReaction.Chat.ID
|
||||
case u.MessageReactionCount != nil:
|
||||
return u.MessageReactionCount.Chat.ID
|
||||
case u.InlineQuery != nil:
|
||||
return u.InlineQuery.From.ID
|
||||
case u.ChosenInlineResult != nil:
|
||||
@@ -63,12 +87,12 @@ func (u Update) ChatID() int64 {
|
||||
return u.ShippingQuery.From.ID
|
||||
case u.PreCheckoutQuery != nil:
|
||||
return u.PreCheckoutQuery.From.ID
|
||||
case u.PollAnswer != nil:
|
||||
return u.PollAnswer.User.ID
|
||||
case u.MyChatMember != nil:
|
||||
return u.MyChatMember.Chat.ID
|
||||
case u.ChatMember != nil:
|
||||
return u.ChatMember.Chat.ID
|
||||
case u.ChatJoinRequest != nil:
|
||||
return u.ChatJoinRequest.Chat.ID
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
@@ -215,9 +239,9 @@ func (a APIResponseString) Base() APIResponseBase {
|
||||
}
|
||||
|
||||
// APIResponseChat represents the incoming response from Telegram servers.
|
||||
// Used by all methods that return a Chat object on success.
|
||||
// Used by all methods that return a ChatFullInfo object on success.
|
||||
type APIResponseChat struct {
|
||||
Result *Chat `json:"result,omitempty"`
|
||||
Result *ChatFullInfo `json:"result,omitempty"`
|
||||
APIResponseBase
|
||||
}
|
||||
|
||||
@@ -443,7 +467,7 @@ func (a APIResponseBotName) Base() APIResponseBase {
|
||||
}
|
||||
|
||||
// APIResponseUserChatBoosts represents the incoming response from Telegram servers.
|
||||
// Used by all methods that return a BotName object on success.
|
||||
// Used by all methods that return a UserChatBoosts object on success.
|
||||
type APIResponseUserChatBoosts struct {
|
||||
Result *UserChatBoosts `json:"result,omitempty"`
|
||||
APIResponseBase
|
||||
@@ -454,6 +478,30 @@ func (a APIResponseUserChatBoosts) Base() APIResponseBase {
|
||||
return a.APIResponseBase
|
||||
}
|
||||
|
||||
// APIResponseBusinessConnection represents the incoming response from Telegram servers.
|
||||
// Used by all methods that return a BusinessConnection object on success.
|
||||
type APIResponseBusinessConnection struct {
|
||||
Result *BusinessConnection `json:"result,omitempty"`
|
||||
APIResponseBase
|
||||
}
|
||||
|
||||
// Base returns the contained object of type APIResponseBase.
|
||||
func (a APIResponseBusinessConnection) Base() APIResponseBase {
|
||||
return a.APIResponseBase
|
||||
}
|
||||
|
||||
// APIResponseStarTransactions represents the incoming response from Telegram servers.
|
||||
// Used by all methods that return a StarTransactions object on success.
|
||||
type APIResponseStarTransactions struct {
|
||||
Result *StarTransactions `json:"result,omitempty"`
|
||||
APIResponseBase
|
||||
}
|
||||
|
||||
// Base returns the contained object of type APIResponseBase.
|
||||
func (a APIResponseStarTransactions) Base() APIResponseBase {
|
||||
return a.APIResponseBase
|
||||
}
|
||||
|
||||
// User represents a Telegram user or bot.
|
||||
type User struct {
|
||||
FirstName string `json:"first_name"`
|
||||
@@ -465,18 +513,35 @@ type User struct {
|
||||
CanJoinGroups bool `json:"can_join_groups,omitempty"`
|
||||
CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"`
|
||||
SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"`
|
||||
CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"`
|
||||
IsPremium bool `json:"is_premium,omitempty"`
|
||||
AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"`
|
||||
}
|
||||
|
||||
// Chat represents a chat.
|
||||
type Chat struct {
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
FirstName string `json:"first_name,omitempty"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
ID int64 `json:"id"`
|
||||
IsForum bool `json:"is_forum,omitempty"`
|
||||
}
|
||||
|
||||
// ChatFullInfo contains full information about a chat.
|
||||
type ChatFullInfo struct {
|
||||
Permissions *ChatPermissions `json:"permissions,omitempty"`
|
||||
Location *ChatLocation `json:"location,omitempty"`
|
||||
PinnedMessage *Message `json:"pinned_message,omitempty"`
|
||||
Photo *ChatPhoto `json:"photo,omitempty"`
|
||||
ActiveUsernames *[]string `json:"active_usernames,omitempty"`
|
||||
AvailableReactions *[]ReactionType `json:"available_reactions,omitempty"`
|
||||
BusinessIntro *BusinessIntro `json:"business_intro,omitempty"`
|
||||
BusinessLocation *BusinessLocation `json:"business_location,omitempty"`
|
||||
BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"`
|
||||
PersonalChat *Chat `json:"personal_chat,omitempty"`
|
||||
Birthdate *Birthdate `json:"birthdate,omitempty"`
|
||||
BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"`
|
||||
ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
@@ -491,6 +556,7 @@ type Chat struct {
|
||||
Type string `json:"type"`
|
||||
CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"`
|
||||
AccentColorID int `json:"accent_color_id,omitempty"`
|
||||
MaxReactionCount int `json:"max_reaction_count,omitempty"`
|
||||
ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"`
|
||||
EmojiStatusExpirationDate int `json:"emoji_status_expiration_date,omitempty"`
|
||||
MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"`
|
||||
@@ -561,12 +627,16 @@ type Message struct {
|
||||
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
|
||||
BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"`
|
||||
ChatBackgroundSet *ChatBackground `json:"chat_background_set,omitempty"`
|
||||
SenderBusinessBot *User `json:"sender_business_bot,omitempty"`
|
||||
MediaGroupID string `json:"media_group_id,omitempty"`
|
||||
ConnectedWebsite string `json:"connected_website,omitempty"`
|
||||
NewChatTitle string `json:"new_chat_title,omitempty"`
|
||||
AuthorSignature string `json:"author_signature,omitempty"`
|
||||
Caption string `json:"caption,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
BusinessConnectionID string `json:"business_connection_id,omitempty"`
|
||||
EffectID string `json:"effect_id,omitempty"`
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
NewChatPhoto []*PhotoSize `json:"new_chat_photo,omitempty"`
|
||||
NewChatMembers []*User `json:"new_chat_members,omitempty"`
|
||||
@@ -588,6 +658,8 @@ type Message struct {
|
||||
ChannelChatCreated bool `json:"channel_chat_created,omitempty"`
|
||||
HasProtectedContent bool `json:"has_protected_content,omitempty"`
|
||||
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
|
||||
IsFromOffline bool `json:"is_from_offline,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// MessageID represents a unique message identifier.
|
||||
@@ -702,9 +774,17 @@ type Dice struct {
|
||||
// PollOption contains information about one answer option in a poll.
|
||||
type PollOption struct {
|
||||
Text string `json:"text"`
|
||||
TextEntities []*MessageEntity `json:"text_entities,omitempty"`
|
||||
VoterCount int `json:"voter_count"`
|
||||
}
|
||||
|
||||
// InputPollOption contains information about one answer option in a poll to send.
|
||||
type InputPollOption struct {
|
||||
Text string `json:"text"`
|
||||
TextParseMode ParseMode `json:"text_parse_mode,omitempty"`
|
||||
TextEntities []*MessageEntity `json:"text_entities,omitempty"`
|
||||
}
|
||||
|
||||
// PollAnswer represents an answer of a user in a non-anonymous poll.
|
||||
type PollAnswer struct {
|
||||
PollID string `json:"poll_id"`
|
||||
@@ -720,6 +800,7 @@ type Poll struct {
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
ID string `json:"id"`
|
||||
ExplanationEntities []*MessageEntity `json:"explanation_entities,omitempty"`
|
||||
QuestionEntities []*MessageEntity `json:"question_entities,omitempty"`
|
||||
Options []*PollOption `json:"options"`
|
||||
OpenPeriod int `json:"open_period,omitempty"`
|
||||
TotalVoterCount int `json:"total_voter_count"`
|
||||
@@ -885,12 +966,13 @@ type ChatMember struct {
|
||||
// ChatMemberUpdated represents changes in the status of a chat member.
|
||||
type ChatMemberUpdated struct {
|
||||
InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
|
||||
Chat Chat `json:"chat"`
|
||||
From User `json:"from"`
|
||||
OldChatMember ChatMember `json:"old_chat_member"`
|
||||
NewChatMember ChatMember `json:"new_chat_member"`
|
||||
Chat Chat `json:"chat"`
|
||||
ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"`
|
||||
Date int `json:"date"`
|
||||
ViaChatFolderInviteLink bool `json:"via_chat_folder_invite_link,omitempty"`
|
||||
ViaJoinRequest bool `json:"via_join_request,omitempty"`
|
||||
}
|
||||
|
||||
// ChatPermissions describes actions that a non-administrator user is allowed to take in a chat.
|
||||
@@ -911,6 +993,38 @@ type ChatPermissions struct {
|
||||
CanManageTopics bool `json:"can_manage_topics,omitempty"`
|
||||
}
|
||||
|
||||
// Birthdate
|
||||
type Birthdate struct {
|
||||
Day int `json:"day"`
|
||||
Month int `json:"month"`
|
||||
Year int `json:"year"`
|
||||
}
|
||||
|
||||
// BusinessIntro
|
||||
type BusinessIntro struct {
|
||||
Sticker *Sticker `json:"sticker,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// BusinessLocation
|
||||
type BusinessLocation struct {
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// BusinessOpeningHoursInterval
|
||||
type BusinessOpeningHoursInterval struct {
|
||||
OpeningMinute int `json:"opening_minute"`
|
||||
ClosingMinute int `json:"closing_minute"`
|
||||
}
|
||||
|
||||
// BusinessOpeningHours
|
||||
type BusinessOpeningHours struct {
|
||||
TimeZoneName string `json:"time_zone_name"`
|
||||
OpeningHours []BusinessOpeningHoursInterval `json:"opening_hours"`
|
||||
}
|
||||
|
||||
// ChatLocation represents a location to which a chat is connected.
|
||||
type ChatLocation struct {
|
||||
Location *Location `json:"location"`
|
||||
@@ -1031,6 +1145,7 @@ type InputMediaPhoto struct {
|
||||
ParseMode ParseMode `json:"parse_mode,omitempty"`
|
||||
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
|
||||
HasSpoiler bool `json:"has_spoiler,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
|
||||
@@ -1056,6 +1171,7 @@ type InputMediaVideo struct {
|
||||
Duration int `json:"duration,omitempty"`
|
||||
SupportsStreaming bool `json:"supports_streaming,omitempty"`
|
||||
HasSpoiler bool `json:"has_spoiler,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
|
||||
@@ -1080,6 +1196,7 @@ type InputMediaAnimation struct {
|
||||
Height int `json:"height,omitempty"`
|
||||
Duration int `json:"duration,omitempty"`
|
||||
HasSpoiler bool `json:"has_spoiler,omitempty"`
|
||||
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
|
||||
}
|
||||
|
||||
// media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
|
||||
@@ -1172,8 +1289,8 @@ type BotName struct {
|
||||
type ChatJoinRequest struct {
|
||||
InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
|
||||
Bio string `json:"bio,omitempty"`
|
||||
From User `json:"user"`
|
||||
Chat Chat `json:"chat"`
|
||||
From User `json:"user"`
|
||||
Date int `json:"date"`
|
||||
UserChatID int64 `json:"user_chat_id"`
|
||||
}
|
||||
@@ -1183,6 +1300,90 @@ type ChatBoostAdded struct {
|
||||
BoostCount int `json:"boost_count"`
|
||||
}
|
||||
|
||||
// BackgroundFill describes the way a background is filled based on the selected colors.
|
||||
type BackgroundFill interface {
|
||||
ImplementsBackgroundFill()
|
||||
}
|
||||
|
||||
// BackgroundFillSolid is a background filled using the selected color.
|
||||
// Type MUST be "solid".
|
||||
type BackgroundFillSolid struct {
|
||||
Type string `json:"type"`
|
||||
Color int `json:"color"`
|
||||
}
|
||||
|
||||
func (b BackgroundFillSolid) ImplementsBackgroundFill() {}
|
||||
|
||||
// BackgroundFillGradient is a background with a gradient fill.
|
||||
// Type MUST be "gradient".
|
||||
type BackgroundFillGradient struct {
|
||||
Type string `json:"type"`
|
||||
TopColor int `json:"top_color"`
|
||||
BottomColor int `json:"bottom_color"`
|
||||
RotationAngle int `json:"rotation_angle"`
|
||||
}
|
||||
|
||||
func (b BackgroundFillGradient) ImplementsBackgroundFill() {}
|
||||
|
||||
// BackgroundFillFreeformGradient is a background with a freeform gradient that rotates after every message in the chat.
|
||||
// Type MUST be "freeform_gradient".
|
||||
type BackgroundFillFreeformGradient struct {
|
||||
Type string `json:"type"`
|
||||
Colors []int `json:"colors"`
|
||||
}
|
||||
|
||||
func (b BackgroundFillFreeformGradient) ImplementsBackgroundFill() {}
|
||||
|
||||
// BackgroundType describes the type of a background.
|
||||
type BackgroundType interface {
|
||||
ImplementsBackgroundType()
|
||||
}
|
||||
|
||||
// BackgroundTypeFill is a background which is automatically filled based on the selected colors.
|
||||
// Type MUST be "fill".
|
||||
type BackgroundTypeFill struct {
|
||||
Fill BackgroundFill `json:"fill"`
|
||||
Type string `json:"type"`
|
||||
DarkThemeDimming int `json:"dark_theme_dimming"`
|
||||
}
|
||||
|
||||
func (b BackgroundTypeFill) ImplementsBackgroundType() {}
|
||||
|
||||
// BackgroundTypeWallpaper is a background which is a wallpaper in the JPEG format.
|
||||
// Type MUST be "wallpaper".
|
||||
type BackgroundTypeWallpaper struct {
|
||||
Type string `json:"type"`
|
||||
Document Document `json:"document"`
|
||||
DarkThemeDimming int `json:"dark_theme_dimming"`
|
||||
IsBlurred bool `json:"is_blurred,omitempty"`
|
||||
IsMoving bool `json:"is_moving,omitempty"`
|
||||
}
|
||||
|
||||
func (b BackgroundTypeWallpaper) ImplementsBackgroundType() {}
|
||||
|
||||
// BackgroundTypePattern is a PNG or TGV (gzipped subset of SVG with MIME type “application/x-tgwallpattern”) pattern
|
||||
// to be combined with the background fill chosen by the user.
|
||||
// Type MUST be "pattern".
|
||||
type BackgroundTypePattern struct {
|
||||
Fill BackgroundFill `json:"fill"`
|
||||
Type string `json:"type"`
|
||||
Document Document `json:"document"`
|
||||
Intensity int `json:"intensity"`
|
||||
IsInverted bool `json:"is_inverted,omitempty"`
|
||||
IsMoving bool `json:"is_moving,omitempty"`
|
||||
}
|
||||
|
||||
func (b BackgroundTypePattern) ImplementsBackgroundType() {}
|
||||
|
||||
// BackgroundTypeChatTheme is taken directly from a built-in chat theme.
|
||||
// Type MUST be "chat_theme".
|
||||
type BackgroundTypeChatTheme struct {
|
||||
Type string `json:"type"`
|
||||
ThemeName string `json:"theme_name"`
|
||||
}
|
||||
|
||||
func (b BackgroundTypeChatTheme) ImplementsBackgroundType() {}
|
||||
|
||||
// ForumTopicCreated represents a service message about a new forum topic created in the chat.
|
||||
type ForumTopicCreated struct {
|
||||
Name string `json:"name"`
|
||||
@@ -1190,6 +1391,11 @@ type ForumTopicCreated struct {
|
||||
IconColor int `json:"icon_color"`
|
||||
}
|
||||
|
||||
// ChatBackground represents a chat background.
|
||||
type ChatBackground struct {
|
||||
Type BackgroundType `json:"type"`
|
||||
}
|
||||
|
||||
// ForumTopicClosed represents a service message about a forum topic closed in the chat.
|
||||
type ForumTopicClosed struct{}
|
||||
|
||||
@@ -1210,8 +1416,8 @@ type GeneralForumTopicUnhidden struct{}
|
||||
|
||||
// WriteAccessAllowed represents a service message about a user allowing a bot added to the attachment menu to write messages.
|
||||
type WriteAccessAllowed struct {
|
||||
FromRequest bool `json:"from_request,omitempty"`
|
||||
WebAppName string `json:"web_app_name,omitempty"`
|
||||
FromRequest bool `json:"from_request,omitempty"`
|
||||
FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1244,6 +1450,9 @@ type UserShared struct {
|
||||
|
||||
// ChatShared contains information about the chat whose identifier was shared with the bot using a KeyboardButtonRequestChat button.
|
||||
type ChatShared struct {
|
||||
Photo *[]PhotoSize `json:"photo,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
RequestID int `json:"request_id"`
|
||||
ChatID int64 `json:"chat_id"`
|
||||
}
|
||||
@@ -1254,28 +1463,12 @@ type Story struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type ReactionType interface {
|
||||
ImplementsReactionType()
|
||||
}
|
||||
|
||||
// ReactionTypeEmoji is based on an emoji.
|
||||
// Type is always "emoji".
|
||||
type ReactionTypeEmoji struct {
|
||||
type ReactionType struct {
|
||||
Type string `json:"type"`
|
||||
Emoji string `json:"emoji"`
|
||||
}
|
||||
|
||||
func (i ReactionTypeEmoji) ImplementsReactionType() {}
|
||||
|
||||
// ReactionTypeCustomEmoji is based on a custom emoji.
|
||||
// Type is always "custom_emoji".
|
||||
type ReactionTypeCustomEmoji struct {
|
||||
Type string `json:"type"`
|
||||
CustomEmoji string `json:"custom_emoji"`
|
||||
}
|
||||
|
||||
func (i ReactionTypeCustomEmoji) ImplementsReactionType() {}
|
||||
|
||||
// ReactionCount represents a reaction added to a message along with the number of times it was added.
|
||||
type ReactionCount struct {
|
||||
Type ReactionType `json:"type"`
|
||||
@@ -1285,71 +1478,71 @@ type ReactionCount struct {
|
||||
// MessageReactionUpdated represents a change of a reaction on a message performed by a user.
|
||||
type MessageReactionUpdated struct {
|
||||
Chat Chat `json:"chat"`
|
||||
MessageID int `json:"message_id"`
|
||||
User User `json:"user,omitempty"`
|
||||
ActorChat Chat `json:"actor_chat,omitempty"`
|
||||
Date int `json:"date"`
|
||||
OldReaction []ReactionType `json:"old_reaction"`
|
||||
NewReaction []ReactionType `json:"new_reaction"`
|
||||
User User `json:"user,omitempty"`
|
||||
MessageID int `json:"message_id"`
|
||||
Date int `json:"date"`
|
||||
}
|
||||
|
||||
// MessageReactionCountUpdated represents reaction changes on a message with anonymous reactions.
|
||||
type MessageReactionCountUpdated struct {
|
||||
Reactions []ReactionCount `json:"reactions"`
|
||||
Chat Chat `json:"chat"`
|
||||
MessageID int `json:"message_id"`
|
||||
Date int `json:"date"`
|
||||
Reactions []ReactionCount `json:"reactions"`
|
||||
}
|
||||
|
||||
// TextQuote contains information about the quoted part of a message that is replied to by the given message.
|
||||
type TextQuote struct {
|
||||
Entities *[]MessageEntity `json:"entities,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Position int `json:"position"`
|
||||
IsManual bool `json:"is_manual,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Entities *[]MessageEntity `json:"entities,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalReplyInfo contains information about a message that is being replied to, which may come from another chat or forum topic.
|
||||
type ExternalReplyInfo struct {
|
||||
Origin MessageOrigin `json:"origin"`
|
||||
Chat Chat `json:"chat,omitempty"`
|
||||
MessageID int `json:"message_id,omitempty"`
|
||||
LinkPreviewOptions LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
Animation Animation `json:"animation,omitempty"`
|
||||
Audio Audio `json:"audio,omitempty"`
|
||||
Document Document `json:"document,omitempty"`
|
||||
Photo []PhotoSize `json:"photo,omitempty"`
|
||||
Sticker Sticker `json:"sticker,omitempty"`
|
||||
Story Story `json:"story,omitempty"`
|
||||
Video Video `json:"video,omitempty"`
|
||||
VideoNote VideoNote `json:"video_note,omitempty"`
|
||||
Voice Voice `json:"voice,omitempty"`
|
||||
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
|
||||
Contact Contact `json:"contact,omitempty"`
|
||||
Dice Dice `json:"dice,omitempty"`
|
||||
Game Game `json:"game,omitempty"`
|
||||
Giveaway Giveaway `json:"giveaway,omitempty"`
|
||||
GiveawayWinners GiveawayWinners `json:"giveaway_winners,omitempty"`
|
||||
Invoice Invoice `json:"invoice,omitempty"`
|
||||
Location Location `json:"location,omitempty"`
|
||||
Poll Poll `json:"poll,omitempty"`
|
||||
Venue Venue `json:"venue,omitempty"`
|
||||
Chat Chat `json:"chat,omitempty"`
|
||||
Document Document `json:"document,omitempty"`
|
||||
Origin MessageOrigin `json:"origin"`
|
||||
Contact Contact `json:"contact,omitempty"`
|
||||
Invoice Invoice `json:"invoice,omitempty"`
|
||||
Dice Dice `json:"dice,omitempty"`
|
||||
LinkPreviewOptions LinkPreviewOptions `json:"link_preview_options,omitempty"`
|
||||
Photo []PhotoSize `json:"photo,omitempty"`
|
||||
Audio Audio `json:"audio,omitempty"`
|
||||
Story Story `json:"story,omitempty"`
|
||||
Voice Voice `json:"voice,omitempty"`
|
||||
VideoNote VideoNote `json:"video_note,omitempty"`
|
||||
Game Game `json:"game,omitempty"`
|
||||
Video Video `json:"video,omitempty"`
|
||||
Animation Animation `json:"animation,omitempty"`
|
||||
Sticker Sticker `json:"sticker,omitempty"`
|
||||
Giveaway Giveaway `json:"giveaway,omitempty"`
|
||||
Poll Poll `json:"poll,omitempty"`
|
||||
GiveawayWinners GiveawayWinners `json:"giveaway_winners,omitempty"`
|
||||
Location Location `json:"location,omitempty"`
|
||||
MessageID int `json:"message_id,omitempty"`
|
||||
HasMediaSpoiler bool `json:"has_media_spoiler,omitempty"`
|
||||
}
|
||||
|
||||
// MessageOrigin describes the origin of a message.
|
||||
type MessageOrigin struct {
|
||||
Date int `json:"date"`
|
||||
SenderChat *Chat `json:"sender_chat,omitempty"`
|
||||
SenderUser *User `json:"sender_user,omitempty"`
|
||||
Type string `json:"type"`
|
||||
SenderUserName string `json:"sender_user_name,omitempty"` // Only for MessageOriginHiddenUser
|
||||
AuthorSignature string `json:"author_signature,omitempty"` // Only for MessageOriginChannel
|
||||
SenderChat *Chat `json:"sender_chat,omitempty"` // Only for MessageOriginChat
|
||||
SenderUser *User `json:"sender_user,omitempty"` // Only for MessageOriginUser
|
||||
SenderUserName string `json:"sender_user_name,omitempty"`
|
||||
AuthorSignature string `json:"author_signature,omitempty"`
|
||||
Date int `json:"date"`
|
||||
}
|
||||
|
||||
// LinkPreviewOptions describes the options used for link preview generation.
|
||||
type LinkPreviewOptions struct {
|
||||
IsDisabled bool `json:"is_disabled,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
IsDisabled bool `json:"is_disabled,omitempty"`
|
||||
PreferSmallMedia bool `json:"prefer_small_media,omitempty"`
|
||||
PreferLargeMedia bool `json:"prefer_large_media,omitempty"`
|
||||
ShowAboveText bool `json:"show_above_text,omitempty"`
|
||||
@@ -1357,27 +1550,36 @@ type LinkPreviewOptions struct {
|
||||
|
||||
// ReplyParameters describes reply parameters for the message that is being sent.
|
||||
type ReplyParameters struct {
|
||||
MessageID int `json:"message_id"`
|
||||
ChatID int64 `json:"chat_id,omitempty"`
|
||||
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
|
||||
Quote string `json:"quote,omitempty"`
|
||||
QuoteParseMode string `json:"quote_parse_mode,omitempty"`
|
||||
QuoteEntities []MessageEntity `json:"quote_entitites,omitempty"`
|
||||
MessageID int `json:"message_id"`
|
||||
ChatID int64 `json:"chat_id,omitempty"`
|
||||
QuotePosition int `json:"quote_position,omitempty"`
|
||||
AllowSendingWithoutReply bool `json:"allow_sending_without_reply,omitempty"`
|
||||
}
|
||||
|
||||
// SharedUser contains information about a user that was shared with the bot using a KeyboardButtonRequestUser button.
|
||||
type SharedUser struct {
|
||||
Photo *[]PhotoSize `json:"photo,omitempty"`
|
||||
FirstName string `json:"firstname,omitempty"`
|
||||
LastName string `json:"lastname,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
// UsersShared contains information about the users whose identifiers were shared with the bot using a KeyboardButtonRequestUsers button.
|
||||
type UsersShared struct {
|
||||
Users []SharedUser `json:"users"`
|
||||
RequestID int `json:"request_id"`
|
||||
UserIDs []int64 `json:"user_ids"`
|
||||
}
|
||||
|
||||
// ChatBoost contains information about a chat boost.
|
||||
type ChatBoost struct {
|
||||
BoostID string `json:"boost_id"`
|
||||
Source ChatBoostSource `json:"source"`
|
||||
AddDate int `json:"add_date"`
|
||||
ExpirationDate int `json:"expiration_date"`
|
||||
Source ChatBoostSource `json:"source"`
|
||||
}
|
||||
|
||||
// ChatBoostSourceType is a custom type for the various chat boost sources.
|
||||
@@ -1392,8 +1594,8 @@ const (
|
||||
|
||||
// ChatBoostSource describes the source of a chat boost.
|
||||
type ChatBoostSource struct {
|
||||
Source ChatBoostSourceType `json:"source"`
|
||||
User *User `json:"user,omitempty"`
|
||||
Source ChatBoostSourceType `json:"source"`
|
||||
GiveawayMessageID int `json:"giveaway_message_id,omitempty"`
|
||||
IsUnclaimed bool `json:"is_unclaimed,omitempty"`
|
||||
}
|
||||
@@ -1406,10 +1608,10 @@ type ChatBoostUpdated struct {
|
||||
|
||||
// ChatBoostRemoved represents a boost removed from a chat.
|
||||
type ChatBoostRemoved struct {
|
||||
Chat Chat `json:"chat"`
|
||||
BoostID string `json:"boost_id"`
|
||||
RemoveDate int `json:"remove_date"`
|
||||
Chat Chat `json:"chat"`
|
||||
Source ChatBoostSource `json:"source"`
|
||||
RemoveDate int `json:"remove_date"`
|
||||
}
|
||||
|
||||
// UserChatBoosts represents a list of boosts added to a chat by a user.
|
||||
@@ -1417,39 +1619,57 @@ type UserChatBoosts struct {
|
||||
Boosts []ChatBoost `json:"boosts"`
|
||||
}
|
||||
|
||||
// BusinessConnection describes the connection of the bot with a business account.
|
||||
type BusinessConnection struct {
|
||||
ID string `json:"id"`
|
||||
User User `json:"user"`
|
||||
UserChatID int64 `json:"user_chat_id"`
|
||||
Date int64 `json:"date"`
|
||||
CanReply bool `json:"can_reply"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
}
|
||||
|
||||
// BusinessMessagesDeleted is received when messages are deleted from a connected business account.
|
||||
type BusinessMessagesDeleted struct {
|
||||
BusinessConnectionID string `json:"business_connection_id"`
|
||||
MessageIDs []int `json:"message_ids"`
|
||||
Chat Chat `json:"chat"`
|
||||
}
|
||||
|
||||
// Giveaway represents a message about a scheduled giveaway.
|
||||
type Giveaway struct {
|
||||
CountryCodes *[]string `json:"country_codes,omitempty"`
|
||||
PrizeDescription string `json:"prize_description,omitempty"`
|
||||
Chats []Chat `json:"chats"`
|
||||
WinnersSelectionDate int `json:"winners_selection_date"`
|
||||
WinnerCount int `json:"winner_count"`
|
||||
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
|
||||
OnlyNewMembers bool `json:"only_new_members,omitempty"`
|
||||
HasPublicWinners bool `json:"has_public_winners,omitempty"`
|
||||
PrizeDescription string `json:"prize_description,omitempty"`
|
||||
CountryCodes *[]string `json:"country_codes,omitempty"`
|
||||
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
|
||||
}
|
||||
|
||||
// GiveawayCreated represents a service message about the creation of a scheduled giveaway. Currently holds no information.
|
||||
// GiveawayCreated represents a service message about the creation of a scheduled giveaway.
|
||||
// Currently holds no information.
|
||||
type GiveawayCreated struct{}
|
||||
|
||||
// GiveawayWinners represents a message about the completion of a giveaway with public winners.
|
||||
type GiveawayWinners struct {
|
||||
PrizeDescription string `json:"prize_description,omitempty"`
|
||||
Chats []Chat `json:"chats"`
|
||||
Winners []User `json:"winners"`
|
||||
GiveawayMessageID int `json:"giveaway_message_id"`
|
||||
WinnersSelectionDate int `json:"winners_selection_date"`
|
||||
WinnerCount int `json:"winner_count"`
|
||||
Winners []User `json:"winners"`
|
||||
AdditionalChatCount int `json:"additional_chat_count,omitempty"`
|
||||
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
|
||||
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
|
||||
OnlyNewMembers bool `json:"only_new_members,omitempty"`
|
||||
WasRefunded bool `json:"was_refunded,omitempty"`
|
||||
PrizeDescription string `json:"prize_description,omitempty"`
|
||||
}
|
||||
|
||||
// GiveawayCompleted represents a service message about the completion of a giveaway without public winners.
|
||||
type GiveawayCompleted struct {
|
||||
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
|
||||
WinnerCount int `json:"winner_count"`
|
||||
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
|
||||
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
|
||||
}
|
||||
|
||||
2
vendor/github.com/NicoNex/echotron/v3/webapp.go
generated
vendored
2
vendor/github.com/NicoNex/echotron/v3/webapp.go
generated
vendored
@@ -53,5 +53,5 @@ func (a API) AnswerWebAppQuery(webAppQueryID string, result InlineQueryResult) (
|
||||
|
||||
vals.Set("web_app_query_id", webAppQueryID)
|
||||
vals.Set("result", string(resultJson))
|
||||
return get[APIResponseSentWebAppMessage](a.base, "answerWebAppQuery", vals)
|
||||
return res, a.client.get(a.base, "answerWebAppQuery", vals, &res)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
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 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.
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
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
|
||||
}
|
||||
2
vendor/github.com/cespare/xxhash/v2/README.md
generated
vendored
2
vendor/github.com/cespare/xxhash/v2/README.md
generated
vendored
@@ -70,3 +70,5 @@ benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
|
||||
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
||||
- [FreeCache](https://github.com/coocood/freecache)
|
||||
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
|
||||
- [Ristretto](https://github.com/dgraph-io/ristretto)
|
||||
- [Badger](https://github.com/dgraph-io/badger)
|
||||
|
||||
29
vendor/github.com/cespare/xxhash/v2/xxhash.go
generated
vendored
29
vendor/github.com/cespare/xxhash/v2/xxhash.go
generated
vendored
@@ -19,10 +19,13 @@ const (
|
||||
// Store the primes in an array as well.
|
||||
//
|
||||
// The consts are used when possible in Go code to avoid MOVs but we need a
|
||||
// contiguous array of the assembly code.
|
||||
// contiguous array for the assembly code.
|
||||
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
|
||||
|
||||
// Digest implements hash.Hash64.
|
||||
//
|
||||
// Note that a zero-valued Digest is not ready to receive writes.
|
||||
// Call Reset or create a Digest using New before calling other methods.
|
||||
type Digest struct {
|
||||
v1 uint64
|
||||
v2 uint64
|
||||
@@ -33,19 +36,31 @@ type Digest struct {
|
||||
n int // how much of mem is used
|
||||
}
|
||||
|
||||
// New creates a new Digest that computes the 64-bit xxHash algorithm.
|
||||
// New creates a new Digest with a zero seed.
|
||||
func New() *Digest {
|
||||
return NewWithSeed(0)
|
||||
}
|
||||
|
||||
// NewWithSeed creates a new Digest with the given seed.
|
||||
func NewWithSeed(seed uint64) *Digest {
|
||||
var d Digest
|
||||
d.Reset()
|
||||
d.ResetWithSeed(seed)
|
||||
return &d
|
||||
}
|
||||
|
||||
// Reset clears the Digest's state so that it can be reused.
|
||||
// It uses a seed value of zero.
|
||||
func (d *Digest) Reset() {
|
||||
d.v1 = primes[0] + prime2
|
||||
d.v2 = prime2
|
||||
d.v3 = 0
|
||||
d.v4 = -primes[0]
|
||||
d.ResetWithSeed(0)
|
||||
}
|
||||
|
||||
// ResetWithSeed clears the Digest's state so that it can be reused.
|
||||
// It uses the given seed to initialize the state.
|
||||
func (d *Digest) ResetWithSeed(seed uint64) {
|
||||
d.v1 = seed + prime1 + prime2
|
||||
d.v2 = seed + prime2
|
||||
d.v3 = seed
|
||||
d.v4 = seed - prime1
|
||||
d.total = 0
|
||||
d.n = 0
|
||||
}
|
||||
|
||||
2
vendor/github.com/cespare/xxhash/v2/xxhash_asm.go
generated
vendored
2
vendor/github.com/cespare/xxhash/v2/xxhash_asm.go
generated
vendored
@@ -6,7 +6,7 @@
|
||||
|
||||
package xxhash
|
||||
|
||||
// Sum64 computes the 64-bit xxHash digest of b.
|
||||
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
|
||||
//
|
||||
//go:noescape
|
||||
func Sum64(b []byte) uint64
|
||||
|
||||
2
vendor/github.com/cespare/xxhash/v2/xxhash_other.go
generated
vendored
2
vendor/github.com/cespare/xxhash/v2/xxhash_other.go
generated
vendored
@@ -3,7 +3,7 @@
|
||||
|
||||
package xxhash
|
||||
|
||||
// Sum64 computes the 64-bit xxHash digest of b.
|
||||
// Sum64 computes the 64-bit xxHash digest of b with a zero seed.
|
||||
func Sum64(b []byte) uint64 {
|
||||
// A simpler version would be
|
||||
// d := New()
|
||||
|
||||
2
vendor/github.com/cespare/xxhash/v2/xxhash_safe.go
generated
vendored
2
vendor/github.com/cespare/xxhash/v2/xxhash_safe.go
generated
vendored
@@ -5,7 +5,7 @@
|
||||
|
||||
package xxhash
|
||||
|
||||
// Sum64String computes the 64-bit xxHash digest of s.
|
||||
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
|
||||
func Sum64String(s string) uint64 {
|
||||
return Sum64([]byte(s))
|
||||
}
|
||||
|
||||
2
vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go
generated
vendored
2
vendor/github.com/cespare/xxhash/v2/xxhash_unsafe.go
generated
vendored
@@ -33,7 +33,7 @@ import (
|
||||
//
|
||||
// See https://github.com/golang/go/issues/42739 for discussion.
|
||||
|
||||
// Sum64String computes the 64-bit xxHash digest of s.
|
||||
// Sum64String computes the 64-bit xxHash digest of s with a zero seed.
|
||||
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
|
||||
func Sum64String(s string) uint64 {
|
||||
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
|
||||
|
||||
2
vendor/github.com/cockroachdb/errors/contexttags/contexttags.go
generated
vendored
2
vendor/github.com/cockroachdb/errors/contexttags/contexttags.go
generated
vendored
@@ -25,7 +25,7 @@ import (
|
||||
// WithContextTags captures the k/v pairs stored in the context via the
|
||||
// `logtags` package and annotates them on the error.
|
||||
//
|
||||
// Only the stromg representation of values remains available. This is
|
||||
// Only the strong representation of values remains available. This is
|
||||
// because the library cannot guarantee that the underlying value is
|
||||
// preserved across the network. To avoid creating a stateful interface
|
||||
// (where the user code needs to know whether an error has traveled
|
||||
|
||||
2
vendor/github.com/cockroachdb/errors/errutil/doc.go
generated
vendored
2
vendor/github.com/cockroachdb/errors/errutil/doc.go
generated
vendored
@@ -14,6 +14,6 @@
|
||||
|
||||
// Package errutil combines primitives from the library, the Go errors
|
||||
// package and github.com/pkg/errors. It aims to serve as drop-in
|
||||
// replacement to gitub.com/pkg/errors and replace the legacy
|
||||
// replacement to github.com/pkg/errors and replace the legacy
|
||||
// implementation of `pgerrors`.
|
||||
package errutil
|
||||
|
||||
7
vendor/github.com/cockroachdb/errors/markers/markers.go
generated
vendored
7
vendor/github.com/cockroachdb/errors/markers/markers.go
generated
vendored
@@ -179,6 +179,13 @@ func IsAny(err error, references ...error) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively try multi-error causes, if applicable.
|
||||
for _, me := range errbase.UnwrapMulti(c) {
|
||||
if IsAny(me, references...) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try harder with marks.
|
||||
|
||||
25
vendor/github.com/cockroachdb/fifo/.gitignore
generated
vendored
Normal file
25
vendor/github.com/cockroachdb/fifo/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
.idea/
|
||||
|
||||
201
vendor/github.com/cockroachdb/fifo/LICENSE
generated
vendored
Normal file
201
vendor/github.com/cockroachdb/fifo/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
vendor/github.com/cockroachdb/fifo/invariants_off.go
generated
vendored
Normal file
21
vendor/github.com/cockroachdb/fifo/invariants_off.go
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2024 The Cockroach Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License.
|
||||
|
||||
//go:build !fifo_invariants
|
||||
|
||||
package fifo
|
||||
|
||||
// invariants is false if we were not built with the "fifo_invariants" build
|
||||
// tag.
|
||||
const invariants = false
|
||||
20
vendor/github.com/cockroachdb/fifo/invariants_on.go
generated
vendored
Normal file
20
vendor/github.com/cockroachdb/fifo/invariants_on.go
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2024 The Cockroach Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License.
|
||||
|
||||
//go:build fifo_invariants
|
||||
|
||||
package fifo
|
||||
|
||||
// invariants is true if we were built with the "fifo_invariants" build tag.
|
||||
const invariants = true
|
||||
162
vendor/github.com/cockroachdb/fifo/queue.go
generated
vendored
Normal file
162
vendor/github.com/cockroachdb/fifo/queue.go
generated
vendored
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright 2024 The Cockroach Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License.
|
||||
|
||||
package fifo
|
||||
|
||||
import "sync"
|
||||
|
||||
// Queue implements an allocation efficient FIFO queue. It is not safe for
|
||||
// concurrent access.
|
||||
//
|
||||
// Note that the queue provides pointer access to the internal storage (via
|
||||
// PeekFront and PushBack) so it must be used with care. These pointers must not
|
||||
// be used once the respective element is popped out of the queue.
|
||||
//
|
||||
// -- Implementation --
|
||||
//
|
||||
// The queue is implemented as a linked list of nodes, where each node is a
|
||||
// small ring buffer. The nodes are allocated using a sync.Pool (a single pool
|
||||
// is created for any given type and is used for all queues of that type).
|
||||
type Queue[T any] struct {
|
||||
len int
|
||||
head, tail *queueNode[T]
|
||||
|
||||
pool *QueueBackingPool[T]
|
||||
}
|
||||
|
||||
// MakeQueue constructs a new Queue.
|
||||
//
|
||||
// The pool should be a singleton object initialized with MakeQueueBackingPool.
|
||||
// A single pool can and should be used by all queues of that type.
|
||||
func MakeQueue[T any](pool *QueueBackingPool[T]) Queue[T] {
|
||||
return Queue[T]{
|
||||
pool: pool,
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the current length of the queue.
|
||||
func (q *Queue[T]) Len() int {
|
||||
return q.len
|
||||
}
|
||||
|
||||
// PushBack adds t to the end of the queue.
|
||||
//
|
||||
// The returned pointer can be used to modify the element while it is in the
|
||||
// queue; it is valid until the element is removed from the queue.
|
||||
func (q *Queue[T]) PushBack(t T) *T {
|
||||
if q.head == nil {
|
||||
q.head = q.pool.get()
|
||||
q.tail = q.head
|
||||
} else if q.tail.IsFull() {
|
||||
newTail := q.pool.get()
|
||||
q.tail.next = newTail
|
||||
q.tail = newTail
|
||||
}
|
||||
q.len++
|
||||
return q.tail.PushBack(t)
|
||||
}
|
||||
|
||||
// PeekFront returns the current head of the queue, or nil if the queue is
|
||||
// empty.
|
||||
//
|
||||
// The result is only valid until the next call to PopFront.
|
||||
func (q *Queue[T]) PeekFront() *T {
|
||||
if q.len == 0 {
|
||||
return nil
|
||||
}
|
||||
return q.head.PeekFront()
|
||||
}
|
||||
|
||||
// PopFront removes the current head of the queue.
|
||||
//
|
||||
// It is illegal to call PopFront on an empty queue.
|
||||
func (q *Queue[T]) PopFront() {
|
||||
q.head.PopFront()
|
||||
// If this is the only node, we don't want to release it; otherwise we would
|
||||
// allocate/free a node every time we transition between the queue being empty
|
||||
// and non-empty.
|
||||
if q.head.len == 0 && q.head.next != nil {
|
||||
oldHead := q.head
|
||||
q.head = oldHead.next
|
||||
q.pool.put(oldHead)
|
||||
}
|
||||
q.len--
|
||||
}
|
||||
|
||||
// QueueBackingPool is a sync.Pool that used to allocate internal nodes
|
||||
// for Queue[T].
|
||||
type QueueBackingPool[T any] struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// MakeQueueBackingPool makes a queue backing pool. It is intented to be used to
|
||||
// initialize a singleton (global) variable. A single pool can and should be
|
||||
// used by all queues of that type.
|
||||
func MakeQueueBackingPool[T any]() QueueBackingPool[T] {
|
||||
return QueueBackingPool[T]{
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} { return &queueNode[T]{} },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (qp *QueueBackingPool[T]) get() *queueNode[T] {
|
||||
return qp.pool.Get().(*queueNode[T])
|
||||
}
|
||||
|
||||
func (qp *QueueBackingPool[T]) put(n *queueNode[T]) {
|
||||
*n = queueNode[T]{}
|
||||
qp.pool.Put(n)
|
||||
}
|
||||
|
||||
// We batch the allocation of this many queue objects. The value was chosen
|
||||
// without experimentation - it provides a reasonable amount of amortization
|
||||
// without a very large increase in memory overhead if T is large.
|
||||
const queueNodeSize = 8
|
||||
|
||||
type queueNode[T any] struct {
|
||||
buf [queueNodeSize]T
|
||||
head, len int32
|
||||
next *queueNode[T]
|
||||
}
|
||||
|
||||
func (qn *queueNode[T]) IsFull() bool {
|
||||
return qn.len == queueNodeSize
|
||||
}
|
||||
|
||||
func (qn *queueNode[T]) PushBack(t T) *T {
|
||||
if invariants && qn.len >= queueNodeSize {
|
||||
panic("cannot push back into a full node")
|
||||
}
|
||||
i := (qn.head + qn.len) % queueNodeSize
|
||||
qn.buf[i] = t
|
||||
qn.len++
|
||||
return &qn.buf[i]
|
||||
}
|
||||
|
||||
func (qn *queueNode[T]) PeekFront() *T {
|
||||
return &qn.buf[qn.head]
|
||||
}
|
||||
|
||||
func (qn *queueNode[T]) PopFront() T {
|
||||
if invariants && qn.len == 0 {
|
||||
panic("cannot pop from empty queue")
|
||||
}
|
||||
t := qn.buf[qn.head]
|
||||
var zero T
|
||||
qn.buf[qn.head] = zero
|
||||
qn.head = (qn.head + 1) % queueNodeSize
|
||||
qn.len--
|
||||
return t
|
||||
}
|
||||
239
vendor/github.com/cockroachdb/fifo/semaphore.go
generated
vendored
Normal file
239
vendor/github.com/cockroachdb/fifo/semaphore.go
generated
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright 2024 The Cockroach Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
// implied. See the License for the specific language governing
|
||||
// permissions and limitations under the License.
|
||||
|
||||
package fifo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Semaphore implements a weighted, dynamically reconfigurable semaphore which
|
||||
// respects context cancellation.
|
||||
//
|
||||
// The semaphore implements a FIFO policy, where Acquire requests are satisfied
|
||||
// in order. This policy provides fairness and prevents starvation but is
|
||||
// susceptible to head-of-line blocking, where a large request that can't be
|
||||
// satisfied blocks many other small requests that could be.
|
||||
type Semaphore struct {
|
||||
mu struct {
|
||||
sync.Mutex
|
||||
|
||||
capacity int64
|
||||
// outstanding can exceed capacity if the capacity is dynamically decreased.
|
||||
outstanding int64
|
||||
|
||||
waiters Queue[semaWaiter]
|
||||
|
||||
// numCanceled is the number of waiters in the waiters queue which have been
|
||||
// canceled. It is used to determine the current number of active waiters in
|
||||
// the queue which is waiters.Len() minus this value.
|
||||
numCanceled int
|
||||
|
||||
// numHadToWait accumulates the total number of Acquire requests which had
|
||||
// to wait because the semaphore was exhausted.
|
||||
numHadToWait int64
|
||||
}
|
||||
}
|
||||
|
||||
// NewSemaphore creates a new semaphore with the given capacity.
|
||||
func NewSemaphore(capacity int64) *Semaphore {
|
||||
if capacity <= 0 {
|
||||
panic("invalid capacity")
|
||||
}
|
||||
s := &Semaphore{}
|
||||
s.mu.capacity = capacity
|
||||
s.mu.waiters = MakeQueue[semaWaiter](&semaQueuePool)
|
||||
return s
|
||||
}
|
||||
|
||||
var semaQueuePool = MakeQueueBackingPool[semaWaiter]()
|
||||
|
||||
var ErrRequestExceedsCapacity = errors.New("request exceeds semaphore capacity")
|
||||
|
||||
// TryAcquire attempts to acquire n units from the semaphore without waiting. On
|
||||
// success, returns true and the caller must later Release the units.
|
||||
func (s *Semaphore) TryAcquire(n int64) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
|
||||
s.mu.outstanding += n
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Acquire n units from the semaphore, waiting if necessary.
|
||||
//
|
||||
// If the context is canceled while we are waiting, returns the context error.
|
||||
// If n exceeds the current capacity, returns ErrRequestExceedsCapacity.
|
||||
// On success, the caller must later Release the units.
|
||||
func (s *Semaphore) Acquire(ctx context.Context, n int64) error {
|
||||
s.mu.Lock()
|
||||
|
||||
// Fast path.
|
||||
if s.numWaitersLocked() == 0 && s.mu.outstanding+n <= s.mu.capacity {
|
||||
s.mu.outstanding += n
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if n > s.mu.capacity {
|
||||
s.mu.Unlock()
|
||||
return ErrRequestExceedsCapacity
|
||||
}
|
||||
|
||||
c := chanSyncPool.Get().(chan error)
|
||||
defer chanSyncPool.Put(c)
|
||||
w := s.mu.waiters.PushBack(semaWaiter{n: n, c: c})
|
||||
s.mu.numHadToWait++
|
||||
s.mu.Unlock()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// We need to check if we raced with a channel notify (which happens under
|
||||
// the lock).
|
||||
select {
|
||||
case err := <-c:
|
||||
// We actually fulfilled or failed the request.
|
||||
return err
|
||||
default:
|
||||
}
|
||||
// Mark the request as canceled.
|
||||
w.c = nil
|
||||
s.mu.numCanceled++
|
||||
// If we are the head of the queue, we may be able to fulfill other waiters.
|
||||
s.processWaitersLocked()
|
||||
return ctx.Err()
|
||||
|
||||
case err := <-c:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Release n units back. These must be units that were acquired by a previous
|
||||
// Acquire call. It is legal to split up or coalesce units when releasing.
|
||||
func (s *Semaphore) Release(n int64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.mu.outstanding -= n
|
||||
if s.mu.outstanding < 0 {
|
||||
panic("releasing more than was acquired")
|
||||
}
|
||||
s.processWaitersLocked()
|
||||
}
|
||||
|
||||
// UpdateCapacity changes the capacity of the semaphore. If the new capacity is
|
||||
// smaller, the already outstanding acquisitions might exceed the new capacity
|
||||
// until they are released.
|
||||
//
|
||||
// If there are Acquire calls that are waiting which are requesting more than
|
||||
// the new capacity, they will error out with ErrRequestExceedsCapacity.
|
||||
func (s *Semaphore) UpdateCapacity(capacity int64) {
|
||||
if capacity <= 0 {
|
||||
panic("invalid capacity")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.mu.capacity = capacity
|
||||
s.processWaitersLocked()
|
||||
}
|
||||
|
||||
// Stats returns the current state of the semaphore.
|
||||
func (s *Semaphore) Stats() SemaphoreStats {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return SemaphoreStats{
|
||||
Capacity: s.mu.capacity,
|
||||
Outstanding: s.mu.outstanding,
|
||||
NumHadToWait: s.mu.numHadToWait,
|
||||
}
|
||||
}
|
||||
|
||||
// SemaphoreStats contains information about the current state of the semaphore.
|
||||
type SemaphoreStats struct {
|
||||
// Capacity is the current capacity of the semaphore.
|
||||
Capacity int64
|
||||
// Outstanding is the number of units that have been acquired. Note that this
|
||||
// can exceed Capacity if the capacity was recently decreased.
|
||||
Outstanding int64
|
||||
// NumHadToWait is the total number of Acquire calls (since the semaphore was
|
||||
// created) that had to wait because the semaphore was exhausted. Useful for
|
||||
// cumulative metrics.
|
||||
NumHadToWait int64
|
||||
|
||||
// TODO(radu): consider keeping track of the total amount of time the
|
||||
// semaphore was exhausted (i.e. there were waiters queued).
|
||||
}
|
||||
|
||||
func (ss SemaphoreStats) String() string {
|
||||
return fmt.Sprintf("capacity: %d, outstanding: %d, num-had-to-wait: %d",
|
||||
ss.Capacity, ss.Outstanding, ss.NumHadToWait)
|
||||
}
|
||||
|
||||
type semaWaiter struct {
|
||||
// n is the amount that the waiter is trying to acquire.
|
||||
n int64
|
||||
// c is the channel on which Acquire is blocked. If the request is canceled,
|
||||
// it is set to nil.
|
||||
c chan error
|
||||
}
|
||||
|
||||
// numWaitersLocked returns how many requests (that have not been canceled) are
|
||||
// waiting in the queue.
|
||||
func (s *Semaphore) numWaitersLocked() int {
|
||||
return s.mu.waiters.Len() - s.mu.numCanceled
|
||||
}
|
||||
|
||||
// processWaitersLocked processes and notifies as many waiters from the head of
|
||||
// the queue as possible.
|
||||
func (s *Semaphore) processWaitersLocked() {
|
||||
for ; s.mu.waiters.Len() > 0; s.mu.waiters.PopFront() {
|
||||
switch w := s.mu.waiters.PeekFront(); {
|
||||
case w.c == nil:
|
||||
// Request was canceled, we can just clean it up.
|
||||
s.mu.numCanceled--
|
||||
if invariants && s.mu.numCanceled < 0 {
|
||||
panic("negative numCanceled")
|
||||
}
|
||||
|
||||
case s.mu.outstanding+w.n <= s.mu.capacity:
|
||||
// Request can be fulfilled.
|
||||
s.mu.outstanding += w.n
|
||||
w.c <- nil
|
||||
|
||||
case w.n > s.mu.capacity:
|
||||
// Request must be failed. This can happen if the capacity was decreased
|
||||
// while the element was queued.
|
||||
w.c <- ErrRequestExceedsCapacity
|
||||
|
||||
default:
|
||||
// Head of the queue needs to wait some more.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// chanSyncPool is used to pool allocations of the channels used to notify
|
||||
// goroutines waiting in Acquire.
|
||||
var chanSyncPool = sync.Pool{
|
||||
New: func() interface{} { return make(chan error, 1) },
|
||||
}
|
||||
1
vendor/github.com/cockroachdb/pebble/compaction.go
generated
vendored
1
vendor/github.com/cockroachdb/pebble/compaction.go
generated
vendored
@@ -2142,7 +2142,6 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) {
|
||||
// calculation even when the WAL is disabled.
|
||||
metrics.BytesIn = metrics.BytesFlushed
|
||||
} else {
|
||||
metrics := c.metrics[0]
|
||||
for i := 0; i < n; i++ {
|
||||
metrics.BytesIn += d.mu.mem.queue[i].logSize
|
||||
}
|
||||
|
||||
20
vendor/github.com/cockroachdb/pebble/db.go
generated
vendored
20
vendor/github.com/cockroachdb/pebble/db.go
generated
vendored
@@ -377,7 +377,7 @@ type DB struct {
|
||||
queue []fileInfo
|
||||
// The number of input bytes to the log. This is the raw size of the
|
||||
// batches written to the WAL, without the overhead of the record
|
||||
// envelopes.
|
||||
// envelopes. Requires DB.mu to be held when read or written.
|
||||
bytesIn uint64
|
||||
// The LogWriter is protected by commitPipeline.mu. This allows log
|
||||
// writes to be performed without holding DB.mu, but requires both
|
||||
@@ -2471,7 +2471,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error {
|
||||
var prevLogSize uint64
|
||||
if !d.opts.DisableWAL {
|
||||
now := time.Now()
|
||||
newLogNum, prevLogSize = d.recycleWAL()
|
||||
newLogNum, prevLogSize = d.rotateWAL()
|
||||
if b != nil {
|
||||
b.commitStats.WALRotationDuration += time.Since(now)
|
||||
}
|
||||
@@ -2543,15 +2543,29 @@ func (d *DB) rotateMemtable(newLogNum FileNum, logSeqNum uint64, prev *memTable)
|
||||
var entry *flushableEntry
|
||||
d.mu.mem.mutable, entry = d.newMemTable(newLogNum, logSeqNum)
|
||||
d.mu.mem.queue = append(d.mu.mem.queue, entry)
|
||||
// d.logSize tracks the log size of the WAL file corresponding to the most
|
||||
// recent flushable. The log size of the previous mutable memtable no longer
|
||||
// applies to the current mutable memtable.
|
||||
//
|
||||
// It's tempting to perform this update in rotateWAL, but that would not be
|
||||
// atomic with the enqueue of the new flushable. A call to DB.Metrics()
|
||||
// could acquire DB.mu after the WAL has been rotated but before the new
|
||||
// memtable has been appended; this would result in omitting the log size of
|
||||
// the most recent flushable.
|
||||
d.logSize.Store(0)
|
||||
d.updateReadStateLocked(nil)
|
||||
if prev.writerUnref() {
|
||||
d.maybeScheduleFlush()
|
||||
}
|
||||
}
|
||||
|
||||
// rotateWAL creates a new write-ahead log, possibly recycling a previous WAL's
|
||||
// files. It returns the file number assigned to the new WAL, and the size of
|
||||
// the previous WAL file.
|
||||
//
|
||||
// Both DB.mu and commitPipeline.mu must be held by the caller. Note that DB.mu
|
||||
// may be released and reacquired.
|
||||
func (d *DB) recycleWAL() (newLogNum FileNum, prevLogSize uint64) {
|
||||
func (d *DB) rotateWAL() (newLogNum FileNum, prevLogSize uint64) {
|
||||
if d.opts.DisableWAL {
|
||||
panic("pebble: invalid function call")
|
||||
}
|
||||
|
||||
22
vendor/github.com/cockroachdb/pebble/ingest.go
generated
vendored
22
vendor/github.com/cockroachdb/pebble/ingest.go
generated
vendored
@@ -1223,7 +1223,12 @@ func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error
|
||||
// We create a new WAL for the flushable instead of reusing the end of
|
||||
// the previous WAL. This simplifies the increment of the minimum
|
||||
// unflushed log number, and also simplifies WAL replay.
|
||||
logNum, _ = d.recycleWAL()
|
||||
var prevLogSize uint64
|
||||
logNum, prevLogSize = d.rotateWAL()
|
||||
// As the rotator of the WAL, we're responsible for updating the
|
||||
// previous flushable queue tail's log size.
|
||||
d.mu.mem.queue[len(d.mu.mem.queue)-1].logSize = prevLogSize
|
||||
|
||||
d.mu.Unlock()
|
||||
err := d.commit.directWrite(b)
|
||||
if err != nil {
|
||||
@@ -1243,13 +1248,14 @@ func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error
|
||||
// the appropriate value below.
|
||||
newLogNum := d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum
|
||||
if !d.opts.DisableWAL {
|
||||
// This is WAL num of the next mutable memtable which comes after the
|
||||
// ingestedFlushable in the flushable queue. The mutable memtable
|
||||
// will be created below.
|
||||
newLogNum, _ = d.recycleWAL()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// newLogNum will be the WAL num of the next mutable memtable which
|
||||
// comes after the ingestedFlushable in the flushable queue. The mutable
|
||||
// memtable will be created below.
|
||||
//
|
||||
// The prevLogSize returned by rotateWAL is the WAL to which the
|
||||
// flushable ingest keys were appended. This intermediary WAL is only
|
||||
// used to record the flushable ingest and nothing else.
|
||||
newLogNum, entry.logSize = d.rotateWAL()
|
||||
}
|
||||
|
||||
currMem := d.mu.mem.mutable
|
||||
|
||||
35
vendor/github.com/cockroachdb/pebble/internal/base/iterator.go
generated
vendored
35
vendor/github.com/cockroachdb/pebble/internal/base/iterator.go
generated
vendored
@@ -7,6 +7,9 @@ package base
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/pebble/internal/humanize"
|
||||
"github.com/cockroachdb/redact"
|
||||
)
|
||||
|
||||
// InternalIterator iterates over a DB's key/value pairs in key order. Unlike
|
||||
@@ -407,3 +410,35 @@ func (s *InternalIteratorStats) Merge(from InternalIteratorStats) {
|
||||
s.SeparatedPointValue.ValueBytes += from.SeparatedPointValue.ValueBytes
|
||||
s.SeparatedPointValue.ValueBytesFetched += from.SeparatedPointValue.ValueBytesFetched
|
||||
}
|
||||
|
||||
func (s *InternalIteratorStats) String() string {
|
||||
return redact.StringWithoutMarkers(s)
|
||||
}
|
||||
|
||||
// SafeFormat implements the redact.SafeFormatter interface.
|
||||
func (s *InternalIteratorStats) SafeFormat(p redact.SafePrinter, verb rune) {
|
||||
p.Printf("blocks: %s cached",
|
||||
humanize.Bytes.Uint64(s.BlockBytesInCache),
|
||||
)
|
||||
if s.BlockBytes != s.BlockBytesInCache || s.BlockReadDuration != 0 {
|
||||
p.Printf(", %s not cached (read time: %s)",
|
||||
humanize.Bytes.Uint64(s.BlockBytes-s.BlockBytesInCache),
|
||||
humanize.FormattedString(s.BlockReadDuration.String()),
|
||||
)
|
||||
}
|
||||
p.Printf("; points: %s", humanize.Count.Uint64(s.PointCount))
|
||||
|
||||
if s.PointsCoveredByRangeTombstones != 0 {
|
||||
p.Printf("(%s tombstoned)", humanize.Count.Uint64(s.PointsCoveredByRangeTombstones))
|
||||
}
|
||||
p.Printf(" (%s keys, %s values)",
|
||||
humanize.Bytes.Uint64(s.KeyBytes),
|
||||
humanize.Bytes.Uint64(s.ValueBytes),
|
||||
)
|
||||
if s.SeparatedPointValue.Count != 0 {
|
||||
p.Printf("; separated: %s (%s, %s fetched)",
|
||||
humanize.Count.Uint64(s.SeparatedPointValue.Count),
|
||||
humanize.Bytes.Uint64(s.SeparatedPointValue.ValueBytes),
|
||||
humanize.Bytes.Uint64(s.SeparatedPointValue.ValueBytesFetched))
|
||||
}
|
||||
}
|
||||
|
||||
78
vendor/github.com/cockroachdb/pebble/iterator.go
generated
vendored
78
vendor/github.com/cockroachdb/pebble/iterator.go
generated
vendored
@@ -154,6 +154,18 @@ func (s *RangeKeyIteratorStats) Merge(o RangeKeyIteratorStats) {
|
||||
s.SkippedPoints += o.SkippedPoints
|
||||
}
|
||||
|
||||
func (s *RangeKeyIteratorStats) String() string {
|
||||
return redact.StringWithoutMarkers(s)
|
||||
}
|
||||
|
||||
// SafeFormat implements the redact.SafeFormatter interface.
|
||||
func (s *RangeKeyIteratorStats) SafeFormat(p redact.SafePrinter, verb rune) {
|
||||
p.Printf("range keys: %s, contained points: %s (%s skipped)",
|
||||
humanize.Count.Uint64(uint64(s.Count)),
|
||||
humanize.Count.Uint64(uint64(s.ContainedPoints)),
|
||||
humanize.Count.Uint64(uint64(s.SkippedPoints)))
|
||||
}
|
||||
|
||||
// LazyValue is a lazy value. See the long comment in base.LazyValue.
|
||||
type LazyValue = base.LazyValue
|
||||
|
||||
@@ -2788,44 +2800,44 @@ func (stats *IteratorStats) String() string {
|
||||
|
||||
// SafeFormat implements the redact.SafeFormatter interface.
|
||||
func (stats *IteratorStats) SafeFormat(s redact.SafePrinter, verb rune) {
|
||||
for i := range stats.ForwardStepCount {
|
||||
switch IteratorStatsKind(i) {
|
||||
case InterfaceCall:
|
||||
s.SafeString("(interface (dir, seek, step): ")
|
||||
case InternalIterCall:
|
||||
s.SafeString(", (internal (dir, seek, step): ")
|
||||
}
|
||||
s.Printf("(fwd, %d, %d), (rev, %d, %d))",
|
||||
redact.Safe(stats.ForwardSeekCount[i]), redact.Safe(stats.ForwardStepCount[i]),
|
||||
redact.Safe(stats.ReverseSeekCount[i]), redact.Safe(stats.ReverseStepCount[i]))
|
||||
}
|
||||
if stats.InternalStats != (InternalIteratorStats{}) {
|
||||
s.SafeString(",\n(internal-stats: ")
|
||||
s.Printf("(block-bytes: (total %s, cached %s, read-time %s)), "+
|
||||
"(points: (count %s, key-bytes %s, value-bytes %s, tombstoned %s))",
|
||||
humanize.Bytes.Uint64(stats.InternalStats.BlockBytes),
|
||||
humanize.Bytes.Uint64(stats.InternalStats.BlockBytesInCache),
|
||||
humanize.FormattedString(stats.InternalStats.BlockReadDuration.String()),
|
||||
humanize.Count.Uint64(stats.InternalStats.PointCount),
|
||||
humanize.Bytes.Uint64(stats.InternalStats.KeyBytes),
|
||||
humanize.Bytes.Uint64(stats.InternalStats.ValueBytes),
|
||||
humanize.Count.Uint64(stats.InternalStats.PointsCoveredByRangeTombstones),
|
||||
if stats.ReverseSeekCount[InterfaceCall] == 0 && stats.ReverseSeekCount[InternalIterCall] == 0 {
|
||||
s.Printf("seeked %s times (%s internal)",
|
||||
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InternalIterCall])),
|
||||
)
|
||||
if stats.InternalStats.SeparatedPointValue.Count != 0 {
|
||||
s.Printf(", (separated: (count %s, bytes %s, fetched %s)))",
|
||||
humanize.Count.Uint64(stats.InternalStats.SeparatedPointValue.Count),
|
||||
humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytes),
|
||||
humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytesFetched))
|
||||
} else {
|
||||
s.Printf(")")
|
||||
s.Printf("seeked %s times (%s fwd/%s rev, internal: %s fwd/%s rev)",
|
||||
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall]+stats.ReverseSeekCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ReverseSeekCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InternalIterCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ReverseSeekCount[InternalIterCall])),
|
||||
)
|
||||
}
|
||||
s.SafeString("; ")
|
||||
|
||||
if stats.ReverseStepCount[InterfaceCall] == 0 && stats.ReverseStepCount[InternalIterCall] == 0 {
|
||||
s.Printf("stepped %s times (%s internal)",
|
||||
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InternalIterCall])),
|
||||
)
|
||||
} else {
|
||||
s.Printf("stepped %s times (%s fwd/%s rev, internal: %s fwd/%s rev)",
|
||||
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InterfaceCall]+stats.ReverseStepCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ReverseStepCount[InterfaceCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ForwardStepCount[InternalIterCall])),
|
||||
humanize.Count.Uint64(uint64(stats.ReverseStepCount[InternalIterCall])),
|
||||
)
|
||||
}
|
||||
|
||||
if stats.InternalStats != (InternalIteratorStats{}) {
|
||||
s.SafeString("; ")
|
||||
stats.InternalStats.SafeFormat(s, verb)
|
||||
}
|
||||
if stats.RangeKeyStats != (RangeKeyIteratorStats{}) {
|
||||
s.SafeString(",\n(range-key-stats: ")
|
||||
s.Printf("(count %d), (contained points: (count %d, skipped %d)))",
|
||||
stats.RangeKeyStats.Count,
|
||||
stats.RangeKeyStats.ContainedPoints,
|
||||
stats.RangeKeyStats.SkippedPoints)
|
||||
s.SafeString(", ")
|
||||
stats.RangeKeyStats.SafeFormat(s, verb)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
48
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go
generated
vendored
48
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go
generated
vendored
@@ -92,6 +92,17 @@ type Settings struct {
|
||||
// out a large chunk of dirty filesystem buffers.
|
||||
BytesPerSync int
|
||||
|
||||
// Local contains fields that are only relevant for files stored on the local
|
||||
// filesystem.
|
||||
Local struct {
|
||||
// TODO(radu): move FSCleaner, NoSyncOnClose, BytesPerSync here.
|
||||
|
||||
// ReadaheadConfigFn is a function used to retrieve the current readahead
|
||||
// mode. This function is run whenever a local object is open for reading.
|
||||
// If it is nil, DefaultReadaheadConfig is used.
|
||||
ReadaheadConfigFn func() ReadaheadConfig
|
||||
}
|
||||
|
||||
// Fields here are set only if the provider is to support remote objects
|
||||
// (experimental).
|
||||
Remote struct {
|
||||
@@ -129,6 +140,43 @@ type Settings struct {
|
||||
}
|
||||
}
|
||||
|
||||
// ReadaheadConfig controls the use of read-ahead.
|
||||
type ReadaheadConfig struct {
|
||||
// Informed is the type of read-ahead for operations that are known to read a
|
||||
// large consecutive chunk of a file.
|
||||
Informed ReadaheadMode
|
||||
|
||||
// Speculative is the type of read-ahead used automatically, when consecutive
|
||||
// reads are detected.
|
||||
Speculative ReadaheadMode
|
||||
}
|
||||
|
||||
// DefaultReadaheadConfig is the readahead config used when ReadaheadConfigFn is
|
||||
// not specified.
|
||||
var DefaultReadaheadConfig = ReadaheadConfig{
|
||||
Informed: FadviseSequential,
|
||||
Speculative: FadviseSequential,
|
||||
}
|
||||
|
||||
// ReadaheadMode indicates the type of read-ahead to use, either for informed
|
||||
// read-ahead (e.g. compactions) or speculative read-ahead.
|
||||
type ReadaheadMode uint8
|
||||
|
||||
const (
|
||||
// NoReadahead disables readahead altogether.
|
||||
NoReadahead ReadaheadMode = iota
|
||||
|
||||
// SysReadahead enables the use of SYS_READAHEAD call to prefetch data.
|
||||
// The prefetch window grows dynamically as consecutive writes are detected.
|
||||
SysReadahead
|
||||
|
||||
// FadviseSequential enables to use of FADV_SEQUENTIAL. For informed
|
||||
// read-ahead, FADV_SEQUENTIAL is used from the beginning. For speculative
|
||||
// read-ahead SYS_READAHEAD is first used until the window reaches the maximum
|
||||
// size, then we siwtch to FADV_SEQUENTIAL.
|
||||
FadviseSequential
|
||||
)
|
||||
|
||||
// DefaultSettings initializes default settings (with no remote storage),
|
||||
// suitable for tests and tools.
|
||||
func DefaultSettings(fs vfs.FS, dirName string) Settings {
|
||||
|
||||
5
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go
generated
vendored
5
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go
generated
vendored
@@ -4,6 +4,8 @@
|
||||
|
||||
package objstorageprovider
|
||||
|
||||
import "github.com/cockroachdb/pebble/internal/invariants"
|
||||
|
||||
const (
|
||||
// Constants for dynamic readahead of data blocks. Note that the size values
|
||||
// make sense as some multiple of the default block size; and they should
|
||||
@@ -81,6 +83,9 @@ func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
|
||||
// Returns a size value (greater than 0) that should be prefetched if readahead
|
||||
// would be beneficial.
|
||||
func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
if invariants.Enabled && rs.maxReadaheadSize == 0 {
|
||||
panic("readaheadState not initialized")
|
||||
}
|
||||
currentReadEnd := offset + blockLength
|
||||
if rs.numReads >= minFileReadsForReadahead {
|
||||
// The minimum threshold of sequential reads to justify reading ahead
|
||||
|
||||
6
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs.go
generated
vendored
6
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs.go
generated
vendored
@@ -31,7 +31,11 @@ func (p *provider) vfsOpenForReading(
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return newFileReadable(file, p.st.FS, filename)
|
||||
readaheadConfig := DefaultReadaheadConfig
|
||||
if f := p.st.Local.ReadaheadConfigFn; f != nil {
|
||||
readaheadConfig = f()
|
||||
}
|
||||
return newFileReadable(file, p.st.FS, readaheadConfig, filename)
|
||||
}
|
||||
|
||||
func (p *provider) vfsCreate(
|
||||
|
||||
46
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go
generated
vendored
46
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go
generated
vendored
@@ -29,11 +29,14 @@ type fileReadable struct {
|
||||
// sequential reads option (see vfsReadHandle).
|
||||
filename string
|
||||
fs vfs.FS
|
||||
readaheadConfig ReadaheadConfig
|
||||
}
|
||||
|
||||
var _ objstorage.Readable = (*fileReadable)(nil)
|
||||
|
||||
func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, error) {
|
||||
func newFileReadable(
|
||||
file vfs.File, fs vfs.FS, readaheadConfig ReadaheadConfig, filename string,
|
||||
) (*fileReadable, error) {
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,6 +46,7 @@ func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable,
|
||||
size: info.Size(),
|
||||
filename: filename,
|
||||
fs: fs,
|
||||
readaheadConfig: readaheadConfig,
|
||||
}
|
||||
invariants.SetFinalizer(r, func(obj interface{}) {
|
||||
if obj.(*fileReadable).file != nil {
|
||||
@@ -76,14 +80,14 @@ func (r *fileReadable) Size() int64 {
|
||||
// NewReadHandle is part of the objstorage.Readable interface.
|
||||
func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
|
||||
rh := readHandlePool.Get().(*vfsReadHandle)
|
||||
rh.r = r
|
||||
rh.rs = makeReadaheadState(fileMaxReadaheadSize)
|
||||
rh.init(r)
|
||||
return rh
|
||||
}
|
||||
|
||||
type vfsReadHandle struct {
|
||||
r *fileReadable
|
||||
rs readaheadState
|
||||
readaheadMode ReadaheadMode
|
||||
|
||||
// sequentialFile holds a file descriptor to the same underlying File,
|
||||
// except with fadvise(FADV_SEQUENTIAL) called on it to take advantage of
|
||||
@@ -108,6 +112,14 @@ var readHandlePool = sync.Pool{
|
||||
},
|
||||
}
|
||||
|
||||
func (rh *vfsReadHandle) init(r *fileReadable) {
|
||||
*rh = vfsReadHandle{
|
||||
r: r,
|
||||
rs: makeReadaheadState(fileMaxReadaheadSize),
|
||||
readaheadMode: r.readaheadConfig.Speculative,
|
||||
}
|
||||
}
|
||||
|
||||
// Close is part of the objstorage.ReadHandle interface.
|
||||
func (rh *vfsReadHandle) Close() error {
|
||||
var err error
|
||||
@@ -121,14 +133,17 @@ func (rh *vfsReadHandle) Close() error {
|
||||
|
||||
// ReadAt is part of the objstorage.ReadHandle interface.
|
||||
func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error {
|
||||
var n int
|
||||
var err error
|
||||
if rh.sequentialFile != nil {
|
||||
// Use OS-level read-ahead.
|
||||
n, err = rh.sequentialFile.ReadAt(p, offset)
|
||||
} else {
|
||||
n, err := rh.sequentialFile.ReadAt(p, offset)
|
||||
if invariants.Enabled && err == nil && n != len(p) {
|
||||
panic("short read")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if rh.readaheadMode != NoReadahead {
|
||||
if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 {
|
||||
if readaheadSize >= fileMaxReadaheadSize {
|
||||
if rh.readaheadMode == FadviseSequential && readaheadSize >= fileMaxReadaheadSize {
|
||||
// We've reached the maximum readahead size. Beyond this point, rely on
|
||||
// OS-level readahead.
|
||||
rh.switchToOSReadahead()
|
||||
@@ -136,8 +151,8 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
|
||||
_ = rh.r.file.Prefetch(offset, readaheadSize)
|
||||
}
|
||||
}
|
||||
n, err = rh.r.file.ReadAt(p, offset)
|
||||
}
|
||||
n, err := rh.r.file.ReadAt(p, offset)
|
||||
if invariants.Enabled && err == nil && n != len(p) {
|
||||
panic("short read")
|
||||
}
|
||||
@@ -146,10 +161,16 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
|
||||
|
||||
// SetupForCompaction is part of the objstorage.ReadHandle interface.
|
||||
func (rh *vfsReadHandle) SetupForCompaction() {
|
||||
rh.readaheadMode = rh.r.readaheadConfig.Informed
|
||||
if rh.readaheadMode == FadviseSequential {
|
||||
rh.switchToOSReadahead()
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *vfsReadHandle) switchToOSReadahead() {
|
||||
if invariants.Enabled && rh.readaheadMode != FadviseSequential {
|
||||
panic("readheadMode not respected")
|
||||
}
|
||||
if rh.sequentialFile != nil {
|
||||
return
|
||||
}
|
||||
@@ -164,8 +185,8 @@ func (rh *vfsReadHandle) switchToOSReadahead() {
|
||||
|
||||
// RecordCacheHit is part of the objstorage.ReadHandle interface.
|
||||
func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {
|
||||
if rh.sequentialFile != nil {
|
||||
// Using OS-level readahead, so do nothing.
|
||||
if rh.sequentialFile != nil || rh.readaheadMode == NoReadahead {
|
||||
// Using OS-level or no readahead, so do nothing.
|
||||
return
|
||||
}
|
||||
rh.rs.recordCacheHit(offset, size)
|
||||
@@ -208,8 +229,7 @@ func UsePreallocatedReadHandle(
|
||||
ctx context.Context, readable objstorage.Readable, rh *PreallocatedReadHandle,
|
||||
) objstorage.ReadHandle {
|
||||
if r, ok := readable.(*fileReadable); ok {
|
||||
// See fileReadable.NewReadHandle.
|
||||
rh.vfsReadHandle = vfsReadHandle{r: r}
|
||||
rh.init(r)
|
||||
return rh
|
||||
}
|
||||
return readable.NewReadHandle(ctx)
|
||||
|
||||
1
vendor/github.com/cockroachdb/pebble/open.go
generated
vendored
1
vendor/github.com/cockroachdb/pebble/open.go
generated
vendored
@@ -310,6 +310,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
|
||||
NoSyncOnClose: opts.NoSyncOnClose,
|
||||
BytesPerSync: opts.BytesPerSync,
|
||||
}
|
||||
providerSettings.Local.ReadaheadConfigFn = opts.Local.ReadaheadConfigFn
|
||||
providerSettings.Remote.StorageFactory = opts.Experimental.RemoteStorage
|
||||
providerSettings.Remote.CreateOnShared = opts.Experimental.CreateOnShared
|
||||
providerSettings.Remote.CreateOnSharedLocator = opts.Experimental.CreateOnSharedLocator
|
||||
|
||||
20
vendor/github.com/cockroachdb/pebble/options.go
generated
vendored
20
vendor/github.com/cockroachdb/pebble/options.go
generated
vendored
@@ -14,11 +14,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/cockroachdb/fifo"
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/internal/cache"
|
||||
"github.com/cockroachdb/pebble/internal/humanize"
|
||||
"github.com/cockroachdb/pebble/internal/keyspan"
|
||||
"github.com/cockroachdb/pebble/internal/manifest"
|
||||
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
|
||||
"github.com/cockroachdb/pebble/objstorage/remote"
|
||||
"github.com/cockroachdb/pebble/rangekey"
|
||||
"github.com/cockroachdb/pebble/sstable"
|
||||
@@ -481,11 +483,25 @@ type Options struct {
|
||||
// The default cache size is 8 MB.
|
||||
Cache *cache.Cache
|
||||
|
||||
// LoadBlockSema, if set, is used to limit the number of blocks that can be
|
||||
// loaded (i.e. read from the filesystem) in parallel. Each load acquires one
|
||||
// unit from the semaphore for the duration of the read.
|
||||
LoadBlockSema *fifo.Semaphore
|
||||
|
||||
// Cleaner cleans obsolete files.
|
||||
//
|
||||
// The default cleaner uses the DeleteCleaner.
|
||||
Cleaner Cleaner
|
||||
|
||||
// Local contains option that pertain to files stored on the local filesystem.
|
||||
Local struct {
|
||||
// ReadaheadConfigFn is a function used to retrieve the current readahead
|
||||
// mode. This function is consulted when a table enters the table cache.
|
||||
ReadaheadConfigFn func() ReadaheadConfig
|
||||
|
||||
// TODO(radu): move BytesPerSync, LoadBlockSema, Cleaner here.
|
||||
}
|
||||
|
||||
// Comparer defines a total ordering over the space of []byte keys: a 'less
|
||||
// than' relationship. The same comparison algorithm must be used for reads
|
||||
// and writes over the lifetime of the DB.
|
||||
@@ -996,6 +1012,9 @@ type Options struct {
|
||||
}
|
||||
}
|
||||
|
||||
// ReadaheadConfig controls the use of read-ahead.
|
||||
type ReadaheadConfig = objstorageprovider.ReadaheadConfig
|
||||
|
||||
// DebugCheckLevels calls CheckLevels on the provided database.
|
||||
// It may be set in the DebugCheck field of Options to check
|
||||
// level invariants whenever a new version is installed.
|
||||
@@ -1713,6 +1732,7 @@ func (o *Options) MakeReaderOptions() sstable.ReaderOptions {
|
||||
var readerOpts sstable.ReaderOptions
|
||||
if o != nil {
|
||||
readerOpts.Cache = o.Cache
|
||||
readerOpts.LoadBlockSema = o.LoadBlockSema
|
||||
readerOpts.Comparer = o.Comparer
|
||||
readerOpts.Filters = o.Filters
|
||||
if o.Merger != nil {
|
||||
|
||||
6
vendor/github.com/cockroachdb/pebble/sstable/options.go
generated
vendored
6
vendor/github.com/cockroachdb/pebble/sstable/options.go
generated
vendored
@@ -5,6 +5,7 @@
|
||||
package sstable
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/fifo"
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/internal/cache"
|
||||
)
|
||||
@@ -111,6 +112,11 @@ type ReaderOptions struct {
|
||||
// The default cache size is a zero-size cache.
|
||||
Cache *cache.Cache
|
||||
|
||||
// LoadBlockSema, if set, is used to limit the number of blocks that can be
|
||||
// loaded (i.e. read from the filesystem) in parallel. Each load acquires one
|
||||
// unit from the semaphore for the duration of the read.
|
||||
LoadBlockSema *fifo.Semaphore
|
||||
|
||||
// User properties specified in this map will not be added to sst.Properties.UserProperties.
|
||||
DeniedUserProperties map[string]struct{}
|
||||
|
||||
|
||||
13
vendor/github.com/cockroachdb/pebble/sstable/reader.go
generated
vendored
13
vendor/github.com/cockroachdb/pebble/sstable/reader.go
generated
vendored
@@ -539,6 +539,15 @@ func (r *Reader) readBlock(
|
||||
}
|
||||
|
||||
// Cache miss.
|
||||
|
||||
if sema := r.opts.LoadBlockSema; sema != nil {
|
||||
if err := sema.Acquire(ctx, 1); err != nil {
|
||||
// An error here can only come from the context.
|
||||
return bufferHandle{}, err
|
||||
}
|
||||
defer sema.Release(1)
|
||||
}
|
||||
|
||||
var compressed cacheValueOrBuf
|
||||
if bufferPool != nil {
|
||||
compressed = cacheValueOrBuf{
|
||||
@@ -571,6 +580,7 @@ func (r *Reader) readBlock(
|
||||
int(bh.Length+blockTrailerLen), readDuration.String())
|
||||
}
|
||||
if stats != nil {
|
||||
stats.BlockBytes += bh.Length
|
||||
stats.BlockReadDuration += readDuration
|
||||
}
|
||||
if err != nil {
|
||||
@@ -628,9 +638,6 @@ func (r *Reader) readBlock(
|
||||
decompressed = transformed
|
||||
}
|
||||
|
||||
if stats != nil {
|
||||
stats.BlockBytes += bh.Length
|
||||
}
|
||||
if decompressed.buf.Valid() {
|
||||
return bufferHandle{b: decompressed.buf}, nil
|
||||
}
|
||||
|
||||
3
vendor/github.com/cockroachdb/pebble/sstable/reader_iter_two_lvl.go
generated
vendored
3
vendor/github.com/cockroachdb/pebble/sstable/reader_iter_two_lvl.go
generated
vendored
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
|
||||
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
|
||||
)
|
||||
|
||||
@@ -177,7 +178,7 @@ func (i *twoLevelIterator) init(
|
||||
_ = i.topLevelIndex.Close()
|
||||
return err
|
||||
}
|
||||
i.dataRH = r.readable.NewReadHandle(ctx)
|
||||
i.dataRH = objstorageprovider.UsePreallocatedReadHandle(ctx, r.readable, &i.dataRHPrealloc)
|
||||
if r.tableFormat >= TableFormatPebblev3 {
|
||||
if r.Properties.NumValueBlocks > 0 {
|
||||
i.vbReader = &valueBlockReader{
|
||||
|
||||
11
vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go
generated
vendored
11
vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go
generated
vendored
@@ -5,6 +5,7 @@
|
||||
package vfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
@@ -38,7 +39,15 @@ func (fs *loggingFS) Create(name string) (File, error) {
|
||||
}
|
||||
|
||||
func (fs *loggingFS) Open(name string, opts ...OpenOption) (File, error) {
|
||||
fs.logFn("open: %s", name)
|
||||
var optsStr string
|
||||
if len(opts) > 0 {
|
||||
optsStr = " (options:"
|
||||
for i := range opts {
|
||||
optsStr += fmt.Sprintf(" %T", opts[i])
|
||||
}
|
||||
optsStr += ")"
|
||||
}
|
||||
fs.logFn("open: %s%s", name, optsStr)
|
||||
f, err := fs.FS.Open(name, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
29
vendor/github.com/getsentry/sentry-go/CHANGELOG.md
generated
vendored
29
vendor/github.com/getsentry/sentry-go/CHANGELOG.md
generated
vendored
@@ -1,5 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## 0.28.1
|
||||
|
||||
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.1.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Implement `http.ResponseWriter` to hook into various parts of the response process ([#837](https://github.com/getsentry/sentry-go/pull/837))
|
||||
|
||||
## 0.28.0
|
||||
|
||||
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.28.0.
|
||||
|
||||
### Features
|
||||
|
||||
- Add a `Fiber` performance tracing & error reporting integration ([#795](https://github.com/getsentry/sentry-go/pull/795))
|
||||
- Add performance tracing to the `Echo` integration ([#722](https://github.com/getsentry/sentry-go/pull/722))
|
||||
- Add performance tracing to the `FastHTTP` integration ([#732](https://github.com/getsentry/sentry-go/pull/723))
|
||||
- Add performance tracing to the `Iris` integration ([#809](https://github.com/getsentry/sentry-go/pull/809))
|
||||
- Add performance tracing to the `Negroni` integration ([#808](https://github.com/getsentry/sentry-go/pull/808))
|
||||
- Add `FailureIssueThreshold` & `RecoveryThreshold` to `MonitorConfig` ([#775](https://github.com/getsentry/sentry-go/pull/775))
|
||||
- Use `errors.Unwrap()` to create exception groups ([#792](https://github.com/getsentry/sentry-go/pull/792))
|
||||
- Add support for matching on strings for `ClientOptions.IgnoreErrors` & `ClientOptions.IgnoreTransactions` ([#819](https://github.com/getsentry/sentry-go/pull/819))
|
||||
- Add `http.request.method` attribute for performance span data ([#786](https://github.com/getsentry/sentry-go/pull/786))
|
||||
- Accept `interface{}` for span data values ([#784](https://github.com/getsentry/sentry-go/pull/784))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix missing stack trace for parsing error in `logrusentry` ([#689](https://github.com/getsentry/sentry-go/pull/689))
|
||||
|
||||
## 0.27.0
|
||||
|
||||
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0.
|
||||
|
||||
2
vendor/github.com/getsentry/sentry-go/README.md
generated
vendored
2
vendor/github.com/getsentry/sentry-go/README.md
generated
vendored
@@ -13,7 +13,6 @@
|
||||
[](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow)
|
||||
[](https://goreportcard.com/report/github.com/getsentry/sentry-go)
|
||||
[](https://discord.gg/Ww9hbqr)
|
||||
[](https://godoc.org/github.com/getsentry/sentry-go)
|
||||
[](https://pkg.go.dev/github.com/getsentry/sentry-go)
|
||||
|
||||
`sentry-go` provides a Sentry client implementation for the Go programming
|
||||
@@ -85,7 +84,6 @@ checkout the official documentation:
|
||||
|
||||
- [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
|
||||
- [GitHub Project](https://github.com/getsentry/sentry-go)
|
||||
- [](https://godoc.org/github.com/getsentry/sentry-go)
|
||||
- [](https://pkg.go.dev/github.com/getsentry/sentry-go)
|
||||
- [](https://docs.sentry.io/platforms/go/)
|
||||
- [](https://github.com/getsentry/sentry-go/discussions)
|
||||
|
||||
4
vendor/github.com/getsentry/sentry-go/check_in.go
generated
vendored
4
vendor/github.com/getsentry/sentry-go/check_in.go
generated
vendored
@@ -87,6 +87,10 @@ type MonitorConfig struct { //nolint: maligned // prefer readability over optima
|
||||
// A tz database string representing the timezone which the monitor's execution schedule is in.
|
||||
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
Timezone string `json:"timezone,omitempty"`
|
||||
// The number of consecutive failed check-ins it takes before an issue is created.
|
||||
FailureIssueThreshold int64 `json:"failure_issue_threshold,omitempty"`
|
||||
// The number of consecutive OK check-ins it takes before an issue is resolved.
|
||||
RecoveryThreshold int64 `json:"recovery_threshold,omitempty"`
|
||||
}
|
||||
|
||||
type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout
|
||||
|
||||
3
vendor/github.com/getsentry/sentry-go/dsn.go
generated
vendored
3
vendor/github.com/getsentry/sentry-go/dsn.go
generated
vendored
@@ -90,11 +90,10 @@ func NewDsn(rawURL string) (*Dsn, error) {
|
||||
// Port
|
||||
var port int
|
||||
if parsedURL.Port() != "" {
|
||||
parsedPort, err := strconv.Atoi(parsedURL.Port())
|
||||
port, err = strconv.Atoi(parsedURL.Port())
|
||||
if err != nil {
|
||||
return nil, &DsnParseError{"invalid port"}
|
||||
}
|
||||
port = parsedPort
|
||||
} else {
|
||||
port = scheme.defaultPort()
|
||||
}
|
||||
|
||||
4
vendor/github.com/getsentry/sentry-go/integrations.go
generated
vendored
4
vendor/github.com/getsentry/sentry-go/integrations.go
generated
vendored
@@ -140,7 +140,7 @@ func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event
|
||||
|
||||
for _, suspect := range suspects {
|
||||
for _, pattern := range iei.ignoreErrors {
|
||||
if pattern.Match([]byte(suspect)) {
|
||||
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
|
||||
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
|
||||
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
||||
return nil
|
||||
@@ -202,7 +202,7 @@ func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint)
|
||||
}
|
||||
|
||||
for _, pattern := range iei.ignoreTransactions {
|
||||
if pattern.Match([]byte(suspect)) {
|
||||
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
|
||||
Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
|
||||
"| Value matched: %s | Filter used: %s", suspect, pattern)
|
||||
return nil
|
||||
|
||||
84
vendor/github.com/getsentry/sentry-go/interfaces.go
generated
vendored
84
vendor/github.com/getsentry/sentry-go/interfaces.go
generated
vendored
@@ -3,6 +3,7 @@ package sentry
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -24,6 +25,9 @@ const profileType = "profile"
|
||||
// checkInType is the type of a check in event.
|
||||
const checkInType = "check_in"
|
||||
|
||||
// metricType is the type of a metric event.
|
||||
const metricType = "statsd"
|
||||
|
||||
// Level marks the severity of the event.
|
||||
type Level string
|
||||
|
||||
@@ -36,15 +40,6 @@ const (
|
||||
LevelFatal Level = "fatal"
|
||||
)
|
||||
|
||||
func getSensitiveHeaders() map[string]bool {
|
||||
return map[string]bool{
|
||||
"Authorization": true,
|
||||
"Cookie": true,
|
||||
"X-Forwarded-For": true,
|
||||
"X-Real-Ip": true,
|
||||
}
|
||||
}
|
||||
|
||||
// SdkInfo contains all metadata about about the SDK being used.
|
||||
type SdkInfo struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
@@ -170,6 +165,13 @@ type Request struct {
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
var sensitiveHeaders = map[string]struct{}{
|
||||
"Authorization": {},
|
||||
"Cookie": {},
|
||||
"X-Forwarded-For": {},
|
||||
"X-Real-Ip": {},
|
||||
}
|
||||
|
||||
// NewRequest returns a new Sentry Request from the given http.Request.
|
||||
//
|
||||
// NewRequest avoids operations that depend on network access. In particular, it
|
||||
@@ -200,7 +202,6 @@ func NewRequest(r *http.Request) *Request {
|
||||
env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
|
||||
}
|
||||
} else {
|
||||
sensitiveHeaders := getSensitiveHeaders()
|
||||
for k, v := range r.Header {
|
||||
if _, ok := sensitiveHeaders[k]; !ok {
|
||||
headers[k] = strings.Join(v, ",")
|
||||
@@ -225,8 +226,12 @@ type Mechanism struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
HelpLink string `json:"help_link,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Handled *bool `json:"handled,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
ParentID *int `json:"parent_id,omitempty"`
|
||||
ExceptionID int `json:"exception_id"`
|
||||
IsExceptionGroup bool `json:"is_exception_group,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// SetUnhandled indicates that the exception is an unhandled exception, i.e.
|
||||
@@ -318,6 +323,7 @@ type Event struct {
|
||||
Exception []Exception `json:"exception,omitempty"`
|
||||
DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
|
||||
Attachments []*Attachment `json:"-"`
|
||||
Metrics []Metric `json:"-"`
|
||||
|
||||
// The fields below are only relevant for transactions.
|
||||
|
||||
@@ -339,27 +345,43 @@ type Event struct {
|
||||
// SetException appends the unwrapped errors to the event's exception list.
|
||||
//
|
||||
// maxErrorDepth is the maximum depth of the error chain we will look
|
||||
// into while unwrapping the errors.
|
||||
// into while unwrapping the errors. If maxErrorDepth is -1, we will
|
||||
// unwrap all errors in the chain.
|
||||
func (e *Event) SetException(exception error, maxErrorDepth int) {
|
||||
err := exception
|
||||
if err == nil {
|
||||
if exception == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < maxErrorDepth && err != nil; i++ {
|
||||
err := exception
|
||||
|
||||
for i := 0; err != nil && (i < maxErrorDepth || maxErrorDepth == -1); i++ {
|
||||
// Add the current error to the exception slice with its details
|
||||
e.Exception = append(e.Exception, Exception{
|
||||
Value: err.Error(),
|
||||
Type: reflect.TypeOf(err).String(),
|
||||
Stacktrace: ExtractStacktrace(err),
|
||||
})
|
||||
switch previous := err.(type) {
|
||||
case interface{ Unwrap() error }:
|
||||
err = previous.Unwrap()
|
||||
case interface{ Cause() error }:
|
||||
err = previous.Cause()
|
||||
default:
|
||||
err = nil
|
||||
|
||||
// Attempt to unwrap the error using the standard library's Unwrap method.
|
||||
// If errors.Unwrap returns nil, it means either there is no error to unwrap,
|
||||
// or the error does not implement the Unwrap method.
|
||||
unwrappedErr := errors.Unwrap(err)
|
||||
|
||||
if unwrappedErr != nil {
|
||||
// The error was successfully unwrapped using the standard library's Unwrap method.
|
||||
err = unwrappedErr
|
||||
continue
|
||||
}
|
||||
|
||||
cause, ok := err.(interface{ Cause() error })
|
||||
if !ok {
|
||||
// We cannot unwrap the error further.
|
||||
break
|
||||
}
|
||||
|
||||
// The error implements the Cause method, indicating it may have been wrapped
|
||||
// using the github.com/pkg/errors package.
|
||||
err = cause.Cause()
|
||||
}
|
||||
|
||||
// Add a trace of the current stack to the most recent error in a chain if
|
||||
@@ -370,8 +392,23 @@ func (e *Event) SetException(exception error, maxErrorDepth int) {
|
||||
e.Exception[0].Stacktrace = NewStacktrace()
|
||||
}
|
||||
|
||||
if len(e.Exception) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// event.Exception should be sorted such that the most recent error is last.
|
||||
reverse(e.Exception)
|
||||
|
||||
for i := range e.Exception {
|
||||
e.Exception[i].Mechanism = &Mechanism{
|
||||
IsExceptionGroup: true,
|
||||
ExceptionID: i,
|
||||
}
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
e.Exception[i].Mechanism.ParentID = Pointer(i - 1)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
|
||||
@@ -492,13 +529,12 @@ func (e *Event) checkInMarshalJSON() ([]byte, error) {
|
||||
|
||||
// NewEvent creates a new Event.
|
||||
func NewEvent() *Event {
|
||||
event := Event{
|
||||
return &Event{
|
||||
Contexts: make(map[string]Context),
|
||||
Extra: make(map[string]interface{}),
|
||||
Tags: make(map[string]string),
|
||||
Modules: make(map[string]string),
|
||||
}
|
||||
return &event
|
||||
}
|
||||
|
||||
// Thread specifies threads that were running at the time of an event.
|
||||
|
||||
7
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
7
vendor/github.com/getsentry/sentry-go/internal/ratelimit/category.go
generated
vendored
@@ -32,15 +32,14 @@ var knownCategories = map[Category]struct{}{
|
||||
|
||||
// String returns the category formatted for debugging.
|
||||
func (c Category) String() string {
|
||||
switch c {
|
||||
case "":
|
||||
if c == "" {
|
||||
return "CategoryAll"
|
||||
default:
|
||||
}
|
||||
|
||||
caser := cases.Title(language.English)
|
||||
rv := "Category"
|
||||
for _, w := range strings.Fields(string(c)) {
|
||||
rv += caser.String(w)
|
||||
}
|
||||
return rv
|
||||
}
|
||||
}
|
||||
|
||||
427
vendor/github.com/getsentry/sentry-go/metrics.go
generated
vendored
Normal file
427
vendor/github.com/getsentry/sentry-go/metrics.go
generated
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
NumberOrString interface {
|
||||
int | string
|
||||
}
|
||||
|
||||
void struct{}
|
||||
)
|
||||
|
||||
var (
|
||||
member void
|
||||
keyRegex = regexp.MustCompile(`[^a-zA-Z0-9_/.-]+`)
|
||||
valueRegex = regexp.MustCompile(`[^\w\d\s_:/@\.{}\[\]$-]+`)
|
||||
unitRegex = regexp.MustCompile(`[^a-z]+`)
|
||||
)
|
||||
|
||||
type MetricUnit struct {
|
||||
unit string
|
||||
}
|
||||
|
||||
func (m MetricUnit) toString() string {
|
||||
return m.unit
|
||||
}
|
||||
|
||||
func NanoSecond() MetricUnit {
|
||||
return MetricUnit{
|
||||
"nanosecond",
|
||||
}
|
||||
}
|
||||
|
||||
func MicroSecond() MetricUnit {
|
||||
return MetricUnit{
|
||||
"microsecond",
|
||||
}
|
||||
}
|
||||
|
||||
func MilliSecond() MetricUnit {
|
||||
return MetricUnit{
|
||||
"millisecond",
|
||||
}
|
||||
}
|
||||
|
||||
func Second() MetricUnit {
|
||||
return MetricUnit{
|
||||
"second",
|
||||
}
|
||||
}
|
||||
|
||||
func Minute() MetricUnit {
|
||||
return MetricUnit{
|
||||
"minute",
|
||||
}
|
||||
}
|
||||
|
||||
func Hour() MetricUnit {
|
||||
return MetricUnit{
|
||||
"hour",
|
||||
}
|
||||
}
|
||||
|
||||
func Day() MetricUnit {
|
||||
return MetricUnit{
|
||||
"day",
|
||||
}
|
||||
}
|
||||
|
||||
func Week() MetricUnit {
|
||||
return MetricUnit{
|
||||
"week",
|
||||
}
|
||||
}
|
||||
|
||||
func Bit() MetricUnit {
|
||||
return MetricUnit{
|
||||
"bit",
|
||||
}
|
||||
}
|
||||
|
||||
func Byte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"byte",
|
||||
}
|
||||
}
|
||||
|
||||
func KiloByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"kilobyte",
|
||||
}
|
||||
}
|
||||
|
||||
func KibiByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"kibibyte",
|
||||
}
|
||||
}
|
||||
|
||||
func MegaByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"megabyte",
|
||||
}
|
||||
}
|
||||
|
||||
func MebiByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"mebibyte",
|
||||
}
|
||||
}
|
||||
|
||||
func GigaByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"gigabyte",
|
||||
}
|
||||
}
|
||||
|
||||
func GibiByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"gibibyte",
|
||||
}
|
||||
}
|
||||
|
||||
func TeraByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"terabyte",
|
||||
}
|
||||
}
|
||||
|
||||
func TebiByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"tebibyte",
|
||||
}
|
||||
}
|
||||
|
||||
func PetaByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"petabyte",
|
||||
}
|
||||
}
|
||||
|
||||
func PebiByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"pebibyte",
|
||||
}
|
||||
}
|
||||
|
||||
func ExaByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"exabyte",
|
||||
}
|
||||
}
|
||||
|
||||
func ExbiByte() MetricUnit {
|
||||
return MetricUnit{
|
||||
"exbibyte",
|
||||
}
|
||||
}
|
||||
|
||||
func Ratio() MetricUnit {
|
||||
return MetricUnit{
|
||||
"ratio",
|
||||
}
|
||||
}
|
||||
|
||||
func Percent() MetricUnit {
|
||||
return MetricUnit{
|
||||
"percent",
|
||||
}
|
||||
}
|
||||
|
||||
func CustomUnit(unit string) MetricUnit {
|
||||
return MetricUnit{
|
||||
unitRegex.ReplaceAllString(unit, ""),
|
||||
}
|
||||
}
|
||||
|
||||
type Metric interface {
|
||||
GetType() string
|
||||
GetTags() map[string]string
|
||||
GetKey() string
|
||||
GetUnit() string
|
||||
GetTimestamp() int64
|
||||
SerializeValue() string
|
||||
SerializeTags() string
|
||||
}
|
||||
|
||||
type abstractMetric struct {
|
||||
key string
|
||||
unit MetricUnit
|
||||
tags map[string]string
|
||||
// A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
|
||||
timestamp int64
|
||||
}
|
||||
|
||||
func (am abstractMetric) GetTags() map[string]string {
|
||||
return am.tags
|
||||
}
|
||||
|
||||
func (am abstractMetric) GetKey() string {
|
||||
return am.key
|
||||
}
|
||||
|
||||
func (am abstractMetric) GetUnit() string {
|
||||
return am.unit.toString()
|
||||
}
|
||||
|
||||
func (am abstractMetric) GetTimestamp() int64 {
|
||||
return am.timestamp
|
||||
}
|
||||
|
||||
func (am abstractMetric) SerializeTags() string {
|
||||
var sb strings.Builder
|
||||
|
||||
values := make([]string, 0, len(am.tags))
|
||||
for k := range am.tags {
|
||||
values = append(values, k)
|
||||
}
|
||||
sortSlice(values)
|
||||
|
||||
for _, key := range values {
|
||||
val := sanitizeValue(am.tags[key])
|
||||
key = sanitizeKey(key)
|
||||
sb.WriteString(fmt.Sprintf("%s:%s,", key, val))
|
||||
}
|
||||
s := sb.String()
|
||||
if len(s) > 0 {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Counter Metric.
|
||||
type CounterMetric struct {
|
||||
value float64
|
||||
abstractMetric
|
||||
}
|
||||
|
||||
func (c *CounterMetric) Add(value float64) {
|
||||
c.value += value
|
||||
}
|
||||
|
||||
func (c CounterMetric) GetType() string {
|
||||
return "c"
|
||||
}
|
||||
|
||||
func (c CounterMetric) SerializeValue() string {
|
||||
return fmt.Sprintf(":%v", c.value)
|
||||
}
|
||||
|
||||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
|
||||
func NewCounterMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) CounterMetric {
|
||||
am := abstractMetric{
|
||||
key,
|
||||
unit,
|
||||
tags,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
return CounterMetric{
|
||||
value,
|
||||
am,
|
||||
}
|
||||
}
|
||||
|
||||
// Distribution Metric.
|
||||
type DistributionMetric struct {
|
||||
values []float64
|
||||
abstractMetric
|
||||
}
|
||||
|
||||
func (d *DistributionMetric) Add(value float64) {
|
||||
d.values = append(d.values, value)
|
||||
}
|
||||
|
||||
func (d DistributionMetric) GetType() string {
|
||||
return "d"
|
||||
}
|
||||
|
||||
func (d DistributionMetric) SerializeValue() string {
|
||||
var sb strings.Builder
|
||||
for _, el := range d.values {
|
||||
sb.WriteString(fmt.Sprintf(":%v", el))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
|
||||
func NewDistributionMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) DistributionMetric {
|
||||
am := abstractMetric{
|
||||
key,
|
||||
unit,
|
||||
tags,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
return DistributionMetric{
|
||||
[]float64{value},
|
||||
am,
|
||||
}
|
||||
}
|
||||
|
||||
// Gauge Metric.
|
||||
type GaugeMetric struct {
|
||||
last float64
|
||||
min float64
|
||||
max float64
|
||||
sum float64
|
||||
count float64
|
||||
abstractMetric
|
||||
}
|
||||
|
||||
func (g *GaugeMetric) Add(value float64) {
|
||||
g.last = value
|
||||
g.min = math.Min(g.min, value)
|
||||
g.max = math.Max(g.max, value)
|
||||
g.sum += value
|
||||
g.count++
|
||||
}
|
||||
|
||||
func (g GaugeMetric) GetType() string {
|
||||
return "g"
|
||||
}
|
||||
|
||||
func (g GaugeMetric) SerializeValue() string {
|
||||
return fmt.Sprintf(":%v:%v:%v:%v:%v", g.last, g.min, g.max, g.sum, g.count)
|
||||
}
|
||||
|
||||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
|
||||
func NewGaugeMetric(key string, unit MetricUnit, tags map[string]string, timestamp int64, value float64) GaugeMetric {
|
||||
am := abstractMetric{
|
||||
key,
|
||||
unit,
|
||||
tags,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
return GaugeMetric{
|
||||
value, // last
|
||||
value, // min
|
||||
value, // max
|
||||
value, // sum
|
||||
value, // count
|
||||
am,
|
||||
}
|
||||
}
|
||||
|
||||
// Set Metric.
|
||||
type SetMetric[T NumberOrString] struct {
|
||||
values map[T]void
|
||||
abstractMetric
|
||||
}
|
||||
|
||||
func (s *SetMetric[T]) Add(value T) {
|
||||
s.values[value] = member
|
||||
}
|
||||
|
||||
func (s SetMetric[T]) GetType() string {
|
||||
return "s"
|
||||
}
|
||||
|
||||
func (s SetMetric[T]) SerializeValue() string {
|
||||
_hash := func(s string) uint32 {
|
||||
return crc32.ChecksumIEEE([]byte(s))
|
||||
}
|
||||
|
||||
values := make([]T, 0, len(s.values))
|
||||
for k := range s.values {
|
||||
values = append(values, k)
|
||||
}
|
||||
sortSlice(values)
|
||||
|
||||
var sb strings.Builder
|
||||
for _, el := range values {
|
||||
switch any(el).(type) {
|
||||
case int:
|
||||
sb.WriteString(fmt.Sprintf(":%v", el))
|
||||
case string:
|
||||
s := fmt.Sprintf("%v", el)
|
||||
sb.WriteString(fmt.Sprintf(":%d", _hash(s)))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// timestamp: A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
|
||||
func NewSetMetric[T NumberOrString](key string, unit MetricUnit, tags map[string]string, timestamp int64, value T) SetMetric[T] {
|
||||
am := abstractMetric{
|
||||
key,
|
||||
unit,
|
||||
tags,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
return SetMetric[T]{
|
||||
map[T]void{
|
||||
value: member,
|
||||
},
|
||||
am,
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeKey(s string) string {
|
||||
return keyRegex.ReplaceAllString(s, "_")
|
||||
}
|
||||
|
||||
func sanitizeValue(s string) string {
|
||||
return valueRegex.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
type Ordered interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string
|
||||
}
|
||||
|
||||
func sortSlice[T Ordered](s []T) {
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
return s[i] < s[j]
|
||||
})
|
||||
}
|
||||
15
vendor/github.com/getsentry/sentry-go/sentry.go
generated
vendored
15
vendor/github.com/getsentry/sentry-go/sentry.go
generated
vendored
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// The version of the SDK.
|
||||
const SDKVersion = "0.27.0"
|
||||
const SDKVersion = "0.28.1"
|
||||
|
||||
// apiVersion is the minimum version of the Sentry API compatible with the
|
||||
// sentry-go SDK.
|
||||
@@ -72,18 +72,17 @@ func Recover() *EventID {
|
||||
|
||||
// RecoverWithContext captures a panic and passes relevant context object.
|
||||
func RecoverWithContext(ctx context.Context) *EventID {
|
||||
if err := recover(); err != nil {
|
||||
var hub *Hub
|
||||
err := recover()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if HasHubOnContext(ctx) {
|
||||
hub = GetHubFromContext(ctx)
|
||||
} else {
|
||||
hub := GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = CurrentHub()
|
||||
}
|
||||
|
||||
return hub.RecoverWithContext(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithScope is a shorthand for CurrentHub().WithScope.
|
||||
|
||||
35
vendor/github.com/getsentry/sentry-go/traces_profiler.go
generated
vendored
35
vendor/github.com/getsentry/sentry-go/traces_profiler.go
generated
vendored
@@ -7,8 +7,8 @@ import (
|
||||
|
||||
// Checks whether the transaction should be profiled (according to ProfilesSampleRate)
|
||||
// and starts a profiler if so.
|
||||
func (span *Span) sampleTransactionProfile() {
|
||||
var sampleRate = span.clientOptions().ProfilesSampleRate
|
||||
func (s *Span) sampleTransactionProfile() {
|
||||
var sampleRate = s.clientOptions().ProfilesSampleRate
|
||||
switch {
|
||||
case sampleRate < 0.0 || sampleRate > 1.0:
|
||||
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate out of range [0.0, 1.0]: %f\n", sampleRate)
|
||||
@@ -19,7 +19,7 @@ func (span *Span) sampleTransactionProfile() {
|
||||
if globalProfiler == nil {
|
||||
Logger.Println("Skipping transaction profiling: the profiler couldn't be started")
|
||||
} else {
|
||||
span.collectProfile = collectTransactionProfile
|
||||
s.collectProfile = collectTransactionProfile
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,22 +69,27 @@ func (info *profileInfo) UpdateFromEvent(event *Event) {
|
||||
info.Dist = event.Dist
|
||||
info.Transaction.ID = event.EventID
|
||||
|
||||
getStringFromContext := func(context map[string]interface{}, originalValue, key string) string {
|
||||
v, ok := context[key]
|
||||
if !ok {
|
||||
return originalValue
|
||||
}
|
||||
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
|
||||
return originalValue
|
||||
}
|
||||
|
||||
if runtimeContext, ok := event.Contexts["runtime"]; ok {
|
||||
if value, ok := runtimeContext["name"]; !ok {
|
||||
info.Runtime.Name = value.(string)
|
||||
}
|
||||
if value, ok := runtimeContext["version"]; !ok {
|
||||
info.Runtime.Version = value.(string)
|
||||
}
|
||||
info.Runtime.Name = getStringFromContext(runtimeContext, info.Runtime.Name, "name")
|
||||
info.Runtime.Version = getStringFromContext(runtimeContext, info.Runtime.Version, "version")
|
||||
}
|
||||
if osContext, ok := event.Contexts["os"]; ok {
|
||||
if value, ok := osContext["name"]; !ok {
|
||||
info.OS.Name = value.(string)
|
||||
}
|
||||
info.OS.Name = getStringFromContext(osContext, info.OS.Name, "name")
|
||||
}
|
||||
if deviceContext, ok := event.Contexts["device"]; ok {
|
||||
if value, ok := deviceContext["arch"]; !ok {
|
||||
info.Device.Architecture = value.(string)
|
||||
}
|
||||
info.Device.Architecture = getStringFromContext(deviceContext, info.Device.Architecture, "arch")
|
||||
}
|
||||
}
|
||||
|
||||
10
vendor/github.com/getsentry/sentry-go/tracing.go
generated
vendored
10
vendor/github.com/getsentry/sentry-go/tracing.go
generated
vendored
@@ -169,11 +169,11 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
|
||||
|
||||
span.Sampled = span.sample()
|
||||
|
||||
span.recorder = &spanRecorder{}
|
||||
if hasParent {
|
||||
span.recorder = parent.spanRecorder()
|
||||
} else {
|
||||
span.recorder = &spanRecorder{}
|
||||
}
|
||||
|
||||
span.recorder.record(&span)
|
||||
|
||||
hub := hubFromContext(ctx)
|
||||
@@ -226,7 +226,11 @@ func (s *Span) SetTag(name, value string) {
|
||||
// SetData sets a data on the span. It is recommended to use SetData instead of
|
||||
// accessing the data map directly as SetData takes care of initializing the map
|
||||
// when necessary.
|
||||
func (s *Span) SetData(name, value string) {
|
||||
func (s *Span) SetData(name string, value interface{}) {
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
||||
108
vendor/github.com/getsentry/sentry-go/transport.go
generated
vendored
108
vendor/github.com/getsentry/sentry-go/transport.go
generated
vendored
@@ -2,6 +2,7 @@ package sentry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -94,6 +95,55 @@ func getRequestBodyFromEvent(event *Event) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalMetrics(metrics []Metric) []byte {
|
||||
var b bytes.Buffer
|
||||
for i, metric := range metrics {
|
||||
b.WriteString(metric.GetKey())
|
||||
if unit := metric.GetUnit(); unit != "" {
|
||||
b.WriteString(fmt.Sprintf("@%s", unit))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("%s|%s", metric.SerializeValue(), metric.GetType()))
|
||||
if serializedTags := metric.SerializeTags(); serializedTags != "" {
|
||||
b.WriteString(fmt.Sprintf("|#%s", serializedTags))
|
||||
}
|
||||
b.WriteString(fmt.Sprintf("|T%d", metric.GetTimestamp()))
|
||||
|
||||
if i < len(metrics)-1 {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func encodeMetric(enc *json.Encoder, b io.Writer, metrics []Metric) error {
|
||||
body := marshalMetrics(metrics)
|
||||
// Item header
|
||||
err := enc.Encode(struct {
|
||||
Type string `json:"type"`
|
||||
Length int `json:"length"`
|
||||
}{
|
||||
Type: metricType,
|
||||
Length: len(body),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// metric payload
|
||||
if _, err = b.Write(body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// "Envelopes should be terminated with a trailing newline."
|
||||
//
|
||||
// [1]: https://develop.sentry.dev/sdk/envelopes/#envelopes
|
||||
if _, err := b.Write([]byte("\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
|
||||
// Attachment header
|
||||
err := enc.Encode(struct {
|
||||
@@ -175,11 +225,15 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if event.Type == transactionType || event.Type == checkInType {
|
||||
switch event.Type {
|
||||
case transactionType, checkInType:
|
||||
err = encodeEnvelopeItem(enc, event.Type, body)
|
||||
} else {
|
||||
case metricType:
|
||||
err = encodeMetric(enc, &b, event.Metrics)
|
||||
default:
|
||||
err = encodeEnvelopeItem(enc, eventType, body)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -206,7 +260,7 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
|
||||
func getRequestFromEvent(ctx context.Context, event *Event, dsn *Dsn) (r *http.Request, err error) {
|
||||
defer func() {
|
||||
if r != nil {
|
||||
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version))
|
||||
@@ -233,7 +287,13 @@ func getRequestFromEvent(event *Event, dsn *Dsn) (r *http.Request, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return http.NewRequest(
|
||||
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
return http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
dsn.GetAPIURL().String(),
|
||||
envelope,
|
||||
@@ -344,8 +404,13 @@ func (t *HTTPTransport) Configure(options ClientOptions) {
|
||||
})
|
||||
}
|
||||
|
||||
// SendEvent assembles a new packet out of Event and sends it to remote server.
|
||||
// SendEvent assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPTransport) SendEvent(event *Event) {
|
||||
t.SendEventWithContext(context.Background(), event)
|
||||
}
|
||||
|
||||
// SendEventWithContext assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event) {
|
||||
if t.dsn == nil {
|
||||
return
|
||||
}
|
||||
@@ -356,7 +421,7 @@ func (t *HTTPTransport) SendEvent(event *Event) {
|
||||
return
|
||||
}
|
||||
|
||||
request, err := getRequestFromEvent(event, t.dsn)
|
||||
request, err := getRequestFromEvent(ctx, event, t.dsn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -478,6 +543,13 @@ func (t *HTTPTransport) worker() {
|
||||
Logger.Printf("There was an issue with sending an event: %v", err)
|
||||
continue
|
||||
}
|
||||
if response.StatusCode >= 400 && response.StatusCode <= 599 {
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
Logger.Printf("Error while reading response code: %v", err)
|
||||
}
|
||||
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.limits.Merge(ratelimit.FromResponse(response))
|
||||
t.mu.Unlock()
|
||||
@@ -567,8 +639,13 @@ func (t *HTTPSyncTransport) Configure(options ClientOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// SendEvent assembles a new packet out of Event and sends it to remote server.
|
||||
// SendEvent assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPSyncTransport) SendEvent(event *Event) {
|
||||
t.SendEventWithContext(context.Background(), event)
|
||||
}
|
||||
|
||||
// SendEventWithContext assembles a new packet out of Event and sends it to the remote server.
|
||||
func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Event) {
|
||||
if t.dsn == nil {
|
||||
return
|
||||
}
|
||||
@@ -577,15 +654,18 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
|
||||
return
|
||||
}
|
||||
|
||||
request, err := getRequestFromEvent(event, t.dsn)
|
||||
request, err := getRequestFromEvent(ctx, event, t.dsn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var eventType string
|
||||
if event.Type == transactionType {
|
||||
switch {
|
||||
case event.Type == transactionType:
|
||||
eventType = "transaction"
|
||||
} else {
|
||||
case event.Type == metricType:
|
||||
eventType = metricType
|
||||
default:
|
||||
eventType = fmt.Sprintf("%s event", event.Level)
|
||||
}
|
||||
Logger.Printf(
|
||||
@@ -601,6 +681,14 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
|
||||
Logger.Printf("There was an issue with sending an event: %v", err)
|
||||
return
|
||||
}
|
||||
if response.StatusCode >= 400 && response.StatusCode <= 599 {
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
Logger.Printf("Error while reading response code: %v", err)
|
||||
}
|
||||
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
t.limits.Merge(ratelimit.FromResponse(response))
|
||||
t.mu.Unlock()
|
||||
|
||||
4
vendor/github.com/getsentry/sentry-go/util.go
generated
vendored
4
vendor/github.com/getsentry/sentry-go/util.go
generated
vendored
@@ -112,3 +112,7 @@ func revisionFromBuildInfo(info *debug.BuildInfo) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func Pointer[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
7
vendor/github.com/klauspost/compress/README.md
generated
vendored
7
vendor/github.com/klauspost/compress/README.md
generated
vendored
@@ -56,6 +56,10 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
|
||||
* flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
|
||||
* gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
|
||||
|
||||
<details>
|
||||
<summary>See changes to v1.16.x</summary>
|
||||
|
||||
|
||||
* July 1st, 2023 - [v1.16.7](https://github.com/klauspost/compress/releases/tag/v1.16.7)
|
||||
* zstd: Fix default level first dictionary encode https://github.com/klauspost/compress/pull/829
|
||||
* s2: add GetBufferCapacity() method by @GiedriusS in https://github.com/klauspost/compress/pull/832
|
||||
@@ -93,6 +97,7 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
|
||||
* s2: Add LZ4 block converter. https://github.com/klauspost/compress/pull/748
|
||||
* s2: Support io.ReaderAt in ReadSeeker. https://github.com/klauspost/compress/pull/747
|
||||
* s2c/s2sx: Use concurrent decoding. https://github.com/klauspost/compress/pull/746
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>See changes to v1.15.x</summary>
|
||||
@@ -560,6 +565,8 @@ the stateless compress described below.
|
||||
|
||||
For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
|
||||
|
||||
To disable all assembly add `-tags=noasm`. This works across all packages.
|
||||
|
||||
# Stateless compression
|
||||
|
||||
This package offers stateless compression as a special option for gzip/deflate.
|
||||
|
||||
2
vendor/github.com/klauspost/compress/internal/snapref/encode_other.go
generated
vendored
2
vendor/github.com/klauspost/compress/internal/snapref/encode_other.go
generated
vendored
@@ -51,7 +51,7 @@ func emitCopy(dst []byte, offset, length int) int {
|
||||
i := 0
|
||||
// The maximum length for a single tagCopy1 or tagCopy2 op is 64 bytes. The
|
||||
// threshold for this loop is a little higher (at 68 = 64 + 4), and the
|
||||
// length emitted down below is is a little lower (at 60 = 64 - 4), because
|
||||
// length emitted down below is a little lower (at 60 = 64 - 4), because
|
||||
// it's shorter to encode a length 67 copy as a length 60 tagCopy2 followed
|
||||
// by a length 7 tagCopy1 (which encodes as 3+2 bytes) than to encode it as
|
||||
// a length 64 tagCopy2 followed by a length 3 tagCopy2 (which encodes as
|
||||
|
||||
3
vendor/github.com/klauspost/compress/zstd/blockdec.go
generated
vendored
3
vendor/github.com/klauspost/compress/zstd/blockdec.go
generated
vendored
@@ -554,6 +554,9 @@ func (b *blockDec) prepareSequences(in []byte, hist *history) (err error) {
|
||||
if debugDecoder {
|
||||
printf("Compression modes: 0b%b", compMode)
|
||||
}
|
||||
if compMode&3 != 0 {
|
||||
return errors.New("corrupt block: reserved bits not zero")
|
||||
}
|
||||
for i := uint(0); i < 3; i++ {
|
||||
mode := seqCompMode((compMode >> (6 - i*2)) & 3)
|
||||
if debugDecoder {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user