0

Compare commits

..

29 Commits

Author SHA1 Message Date
eleksir
124009613e [WIP] Make Makefile crossplatform 2025-11-09 12:50:10 +03:00
eleksir
ddcc8b1c87 [WIP] MOAR linter fixes, also move init to tg lib 2025-11-09 12:25:51 +03:00
eleksir
bb2425f404 [WIP] Add in TODO.org item about settings migrator scripts
* Add item in TODO.org about settings migrator
* Add item about crossplatform Makefile build
* Wrote perl-stub for settings export
* Wrote GO-stub for settings import
2025-11-04 22:52:28 +03:00
eleksir
af7ff57279 [WIP] for SendGoodMorning make sense to iterate over chats only, introduce one more slice for it
* Change mechanics of SendGoodMorning
* Allocate one more slice chatGroupList to store groups/supergroups/channels
* Update TODO.org
2025-11-04 22:30:47 +03:00
eleksir
5471e09a24 [WIP] Extend used orgmode syntax 2025-11-04 15:20:15 +03:00
eleksir
b50c0612a6 [WIP] TODO.md->TODO.org 2025-11-04 14:29:02 +03:00
eleksir
1e54dcf9f1 [WIP] Implement good morning message for all willing chats, update TODO.md 2025-11-02 15:59:43 +03:00
eleksir
61208f5247 [WIP] Add cron-like job for periodic maintenance of settings db, update TODO.md 2025-11-02 12:22:37 +03:00
eleksir
fbd0196a48 [WIP] update linter rules, fix many linter warnings 2025-11-01 02:15:08 +03:00
eleksir
c31547ce4a [WIP] Add missing cmd dir, fix makefile, fix sample_config.json, update .gitignore 2025-10-31 20:17:31 +03:00
eleksir
490677de40 [WIP] Move to go project layout, remove logrus & co from vendored libs 2025-10-31 20:17:31 +03:00
eleksir
d8a92b37fc [WIP] Replace logrus with slog 2025-10-31 20:17:31 +03:00
eleksir
c149b92c0c [WIP] Update TODO.md 2025-10-31 20:17:31 +03:00
eleksir
5a54572b91 [WIP] Implemented !admin chan_msg setting command 2025-10-31 20:17:31 +03:00
eleksir
f427dca430 [WIP] Add comands "weather", "погода", "w", "п", "погодка", "погадка" 2025-10-31 20:17:31 +03:00
eleksir
2af4cdd706 [WIP] Update TODO 2025-10-31 20:17:31 +03:00
eleksir
0c22128c77 [WIP] Implemented save goodbye message option 2025-10-31 20:17:31 +03:00
eleksir
037b07a00d [WIP] Update TODO 2025-10-31 20:17:31 +03:00
eleksir
64da7d6ce7 [WIP] Update TODO 2025-10-31 20:17:22 +03:00
eleksir
2bf30fc346 [WIP] Insert returns in if-s, to stop processing at proper phase, add greet for newcommers 2025-01-02 15:11:50 +03:00
eleksir
abbf2ca145 [WIP] Fix typos 2025-01-02 15:09:42 +03:00
eleksir
312153ad34 [WIP] Add comment for help message 2025-01-02 11:31:04 +03:00
eleksir
0472e0f5a8 [WIP] help command parser moved to separate func, implemented admin {greet,fortune,censor} 2024-08-03 19:32:54 +03:00
eleksir
a2e34ee12d [WIP] Add .editorconfig 2024-07-26 00:58:50 +03:00
eleksir
12eab9c5dc [WIP] Remove ReadConfig(), we already use parseConfig() for this, add vim modelines 2024-07-26 00:58:40 +03:00
eleksir
a6960d0e86 [WIP] Update deps 2024-07-24 22:38:25 +03:00
eleksir
618110a7b2 [WIP] Add censor, deejay some funcs from msg-parsers.go to telegram.go 2024-07-24 19:31:39 +03:00
eleksir
4ed399feb3 [WIP] Fix lint warns, properly format private msg response, implement help command
* Fix/suppress misc linter warnings. Most of them will vanish after TODO will be done.
* Check if incoming message is command (this check also implement format flag for outgoing message)
* Implement help
2024-07-24 16:51:00 +03:00
eleksir
e725c31ca1 [WIP] fix linter complains to use switch..case instead of if..then..else, fix SaveSetting() name typo 2024-07-24 16:45:08 +03:00
316 changed files with 18842 additions and 10259 deletions

20
.editorconfig Normal file
View 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
View File

@@ -1,6 +1,9 @@
.idea/** .idea/**
.vscode/** .vscode/**
.vstags .vstags
data/config.json /data/config.json
data/*_db /data/*_db
aleesa-telegram-go /data/cache
/aleesa-telegram-go
/*.log
/*.bck.*

View File

@@ -1,28 +1,12 @@
version: "2"
run: run:
tests: false go: "1.25"
skip-dirs:
- data
- vendor
modules-download-mode: readonly modules-download-mode: readonly
tests: false
allow-parallel-runners: true allow-parallel-runners: true
go: '1.21'
output:
sort-results: true
linters-settings:
gosec:
excludes:
# Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
- G404 # Insecure random number source (rand)
linters: linters:
disable-all: true default: none
enable: enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- asasalint - asasalint
- asciicheck - asciicheck
- bodyclose - bodyclose
@@ -30,24 +14,30 @@ linters:
- contextcheck - contextcheck
- decorder - decorder
- durationcheck - durationcheck
- errcheck
- errchkjson - errchkjson
- errorlint - errorlint
- exhaustive - exhaustive
- exportloopref - exptostd
- gci
- ginkgolinter - ginkgolinter
- gocheckcompilerdirectives - gocheckcompilerdirectives
- gocritic - gocritic
- godoclint
- godot - godot
- gofmt - godox
- goimports - goprintffuncname
- gosec - gosec
- gosmopolitan - gosmopolitan
- govet
- grouper - grouper
- importas - importas
- ineffassign
- intrange
- ireturn - ireturn
- makezero - makezero
- mirror - mirror
- misspell
- modernize
- nakedret - nakedret
- nilerr - nilerr
- nilnil - nilnil
@@ -55,13 +45,714 @@ linters:
- noctx - noctx
- nolintlint - nolintlint
- nonamedreturns - nonamedreturns
- perfsprint
- predeclared - predeclared
- promlinter - promlinter
- reassign - reassign
- revive - revive
- rowserrcheck - rowserrcheck
- stylecheck - staticcheck
- unconvert - unconvert
- unused
- usestdlibvars
- wastedassign - wastedassign
- whitespace - whitespace
- wsl - wsl_v5
settings:
decorder:
dec-order:
- type
- const
- var
- func
gocritic:
disable-all: false
enable-all: true
disabled-checks:
- singleCaseSwitch
- octalLiteral
- regexpSimplify
- paramTypeCombine
- whyNoLint
- hugeParam
godoclint:
default: all
ignore-unexported: false
ignore-exported: false
include-unexported: true
disable:
- max-len
godot:
scope: all
exclude:
# Exclude todo and fixme comments.
- "^fixme:"
- "^todo:"
period: true
capital: false
godox:
keywords:
- FIXME
gosec: # go-fucking-sic
excludes:
# Боже, как же вы затрахали своей крипто-ебучкой, наркоманы хреновы!
- G405 # Detect the usage of DES or RC4
- G406 # Detect the usage of MD4 or RIPEMD160
# Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки
# произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью.
- G404 # Insecure random number source (rand)
- G501 # Import blocklist: crypto/md5
- G502 # Import blocklist: crypto/des
- G503 # Import blocklist: crypto/rc4
- G504 # Import blocklist: net/http/cgi
- G505 # Import blocklist: crypto/sha1
- G506 # Import blocklist: golang.org/x/crypto/md4
- G507 # Import blocklist: golang.org/x/crypto/ripemd160
config:
global:
audit: true
# Ваша сраная паранойя при создании файлов и каталогов мешает нормальной работе сервисов. Группы придуманы для
# удобства, группе удобно давать право чтения. Поэтому идите нахуй со своими ограничениями на чтение для
# группы, "безопасные" клоуны.
G301: "0755"
G306: "0644"
govet:
disable-all: false
enable-all: true
disable:
- shadow
grouper:
# Require the use of a single global 'const' declaration only.
# Default: false
const-require-single-const: true
# Require the use of grouped global 'const' declarations.
# Default: false
const-require-grouping: true
# Require the use of a single 'import' declaration only.
# Default: false
import-require-single-import: true
# Require the use of grouped 'import' declarations.
# Default: false
import-require-grouping: true
# Require the use of a single global 'type' declaration only.
# Default: false
type-require-single-type: true
# Require the use of grouped global 'type' declarations.
# Default: false
type-require-grouping: true
# Require the use of a single global 'var' declaration only.
# Default: false
var-require-single-var: true
# Require the use of grouped global 'var' declarations.
# Default: false
var-require-grouping: true
misspell:
# Correct spellings using locale preferences for US or UK.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
# Default is to use a neutral variety of English.
locale: US
# Typos to ignore.
# Should be in lower case.
# Default: []
ignore-rules:
- someword
# Extra word corrections.
# `typo` and `correction` should only contain letters.
# The words are case-insensitive.
# Default: []
extra-words:
- typo: "iff"
correction: "if"
- typo: "cancelation"
correction: "cancellation"
# Mode of the analysis:
# - default: checks all the file content.
# - restricted: checks only comments.
# Default: ""
mode: restricted
nonamedreturns:
# Report named error if it is assigned inside defer.
# Default: false
report-error-in-defer: true
perfsprint:
integer-format: true
int-conversion: true
error-format: true
err-error: false
errorf: true
string-format: true
sprintf1: true
strconcat: true
bool-format: true
hex-format: true
concat-loop: true
loop-other-ops: false
revive:
enable-all-rules: false
staticcheck:
# https://staticcheck.dev/docs/configuration/options/#dot_import_whitelist
# Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"]
dot-import-whitelist:
- fmt
# https://staticcheck.dev/docs/configuration/options/#initialisms
# Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"]
initialisms: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"]
# https://staticcheck.dev/docs/configuration/options/#http_status_code_whitelist
# Default: ["200", "400", "404", "500"]
http-status-code-whitelist: ["200", "400", "404", "500"]
# SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks
# Example (to disable some checks): [ "all", "-SA1000", "-SA1001"]
# Run `GL_DEBUG=staticcheck golangci-lint run --enable=staticcheck` to see all available checks and enabled by config checks.
# Default: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
checks:
# Invalid regular expression.
# https://staticcheck.dev/docs/checks/#SA1000
- SA1000
# Invalid template.
# https://staticcheck.dev/docs/checks/#SA1001
- SA1001
# Invalid format in 'time.Parse'.
# https://staticcheck.dev/docs/checks/#SA1002
- SA1002
# Unsupported argument to functions in 'encoding/binary'.
# https://staticcheck.dev/docs/checks/#SA1003
- SA1003
# Suspiciously small untyped constant in 'time.Sleep'.
# https://staticcheck.dev/docs/checks/#SA1004
- SA1004
# Invalid first argument to 'exec.Command'.
# https://staticcheck.dev/docs/checks/#SA1005
- SA1005
# 'Printf' with dynamic first argument and no further arguments.
# https://staticcheck.dev/docs/checks/#SA1006
- SA1006
# Invalid URL in 'net/url.Parse'.
# https://staticcheck.dev/docs/checks/#SA1007
- SA1007
# Non-canonical key in 'http.Header' map.
# https://staticcheck.dev/docs/checks/#SA1008
- SA1008
# '(*regexp.Regexp).FindAll' called with 'n == 0', which will always return zero results.
# https://staticcheck.dev/docs/checks/#SA1010
- SA1010
# Various methods in the "strings" package expect valid UTF-8, but invalid input is provided.
# https://staticcheck.dev/docs/checks/#SA1011
- SA1011
# A nil 'context.Context' is being passed to a function, consider using 'context.TODO' instead.
# https://staticcheck.dev/docs/checks/#SA1012
- SA1012
# 'io.Seeker.Seek' is being called with the whence constant as the first argument, but it should be the second.
# https://staticcheck.dev/docs/checks/#SA1013
- SA1013
# Non-pointer value passed to 'Unmarshal' or 'Decode'.
# https://staticcheck.dev/docs/checks/#SA1014
- SA1014
# Using 'time.Tick' in a way that will leak. Consider using 'time.NewTicker', and only use 'time.Tick' in tests, commands and endless functions.
# https://staticcheck.dev/docs/checks/#SA1015
- SA1015
# Trapping a signal that cannot be trapped.
# https://staticcheck.dev/docs/checks/#SA1016
- SA1016
# Channels used with 'os/signal.Notify' should be buffered.
# https://staticcheck.dev/docs/checks/#SA1017
- SA1017
# 'strings.Replace' called with 'n == 0', which does nothing.
# https://staticcheck.dev/docs/checks/#SA1018
- SA1018
# Using a deprecated function, variable, constant or field.
# https://staticcheck.dev/docs/checks/#SA1019
- SA1019
# Using an invalid host:port pair with a 'net.Listen'-related function.
# https://staticcheck.dev/docs/checks/#SA1020
- SA1020
# Using 'bytes.Equal' to compare two 'net.IP'.
# https://staticcheck.dev/docs/checks/#SA1021
- SA1021
# Modifying the buffer in an 'io.Writer' implementation.
# https://staticcheck.dev/docs/checks/#SA1023
- SA1023
# A string cutset contains duplicate characters.
# https://staticcheck.dev/docs/checks/#SA1024
- SA1024
# It is not possible to use '(*time.Timer).Reset''s return value correctly.
# https://staticcheck.dev/docs/checks/#SA1025
- SA1025
# Cannot marshal channels or functions.
# https://staticcheck.dev/docs/checks/#SA1026
- SA1026
# Atomic access to 64-bit variable must be 64-bit aligned.
# https://staticcheck.dev/docs/checks/#SA1027
- SA1027
# 'sort.Slice' can only be used on slices.
# https://staticcheck.dev/docs/checks/#SA1028
- SA1028
# Inappropriate key in call to 'context.WithValue'.
# https://staticcheck.dev/docs/checks/#SA1029
- SA1029
# Invalid argument in call to a 'strconv' function.
# https://staticcheck.dev/docs/checks/#SA1030
- SA1030
# Overlapping byte slices passed to an encoder.
# https://staticcheck.dev/docs/checks/#SA1031
- SA1031
# Wrong order of arguments to 'errors.Is'.
# https://staticcheck.dev/docs/checks/#SA1032
- SA1032
# 'sync.WaitGroup.Add' called inside the goroutine, leading to a race condition.
# https://staticcheck.dev/docs/checks/#SA2000
- SA2000
# Empty critical section, did you mean to defer the unlock?.
# https://staticcheck.dev/docs/checks/#SA2001
- SA2001
# Called 'testing.T.FailNow' or 'SkipNow' in a goroutine, which isn't allowed.
# https://staticcheck.dev/docs/checks/#SA2002
- SA2002
# Deferred 'Lock' right after locking, likely meant to defer 'Unlock' instead.
# https://staticcheck.dev/docs/checks/#SA2003
- SA2003
# 'TestMain' doesn't call 'os.Exit', hiding test failures.
# https://staticcheck.dev/docs/checks/#SA3000
- SA3000
# Assigning to 'b.N' in benchmarks distorts the results.
# https://staticcheck.dev/docs/checks/#SA3001
- SA3001
# Binary operator has identical expressions on both sides.
# https://staticcheck.dev/docs/checks/#SA4000
- SA4000
# '&*x' gets simplified to 'x', it does not copy 'x'.
# https://staticcheck.dev/docs/checks/#SA4001
- SA4001
# Comparing unsigned values against negative values is pointless.
# https://staticcheck.dev/docs/checks/#SA4003
- SA4003
# The loop exits unconditionally after one iteration.
# https://staticcheck.dev/docs/checks/#SA4004
- SA4004
# Field assignment that will never be observed. Did you mean to use a pointer receiver?.
# https://staticcheck.dev/docs/checks/#SA4005
- SA4005
# A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?.
# https://staticcheck.dev/docs/checks/#SA4006
- SA4006
# The variable in the loop condition never changes, are you incrementing the wrong variable?.
# https://staticcheck.dev/docs/checks/#SA4008
- SA4008
# A function argument is overwritten before its first use.
# https://staticcheck.dev/docs/checks/#SA4009
- SA4009
# The result of 'append' will never be observed anywhere.
# https://staticcheck.dev/docs/checks/#SA4010
- SA4010
# Break statement with no effect. Did you mean to break out of an outer loop?.
# https://staticcheck.dev/docs/checks/#SA4011
- SA4011
# Comparing a value against NaN even though no value is equal to NaN.
# https://staticcheck.dev/docs/checks/#SA4012
- SA4012
# Negating a boolean twice ('!!b') is the same as writing 'b'. This is either redundant, or a typo.
# https://staticcheck.dev/docs/checks/#SA4013
- SA4013
# An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either.
# https://staticcheck.dev/docs/checks/#SA4014
- SA4014
# Calling functions like 'math.Ceil' on floats converted from integers doesn't do anything useful.
# https://staticcheck.dev/docs/checks/#SA4015
- SA4015
# Certain bitwise operations, such as 'x ^ 0', do not do anything useful.
# https://staticcheck.dev/docs/checks/#SA4016
- SA4016
# Discarding the return values of a function without side effects, making the call pointless.
# https://staticcheck.dev/docs/checks/#SA4017
- SA4017
# Self-assignment of variables.
# https://staticcheck.dev/docs/checks/#SA4018
- SA4018
# Multiple, identical build constraints in the same file.
# https://staticcheck.dev/docs/checks/#SA4019
- SA4019
# Unreachable case clause in a type switch.
# https://staticcheck.dev/docs/checks/#SA4020
- SA4020
# "x = append(y)" is equivalent to "x = y".
# https://staticcheck.dev/docs/checks/#SA4021
- SA4021
# Comparing the address of a variable against nil.
# https://staticcheck.dev/docs/checks/#SA4022
- SA4022
# Impossible comparison of interface value with untyped nil.
# https://staticcheck.dev/docs/checks/#SA4023
- SA4023
# Checking for impossible return value from a builtin function.
# https://staticcheck.dev/docs/checks/#SA4024
- SA4024
# Integer division of literals that results in zero.
# https://staticcheck.dev/docs/checks/#SA4025
- SA4025
# Go constants cannot express negative zero.
# https://staticcheck.dev/docs/checks/#SA4026
- SA4026
# '(*net/url.URL).Query' returns a copy, modifying it doesn't change the URL.
# https://staticcheck.dev/docs/checks/#SA4027
- SA4027
# 'x % 1' is always zero.
# https://staticcheck.dev/docs/checks/#SA4028
- SA4028
# Ineffective attempt at sorting slice.
# https://staticcheck.dev/docs/checks/#SA4029
- SA4029
# Ineffective attempt at generating random number.
# https://staticcheck.dev/docs/checks/#SA4030
- SA4030
# Checking never-nil value against nil.
# https://staticcheck.dev/docs/checks/#SA4031
- SA4031
# Comparing 'runtime.GOOS' or 'runtime.GOARCH' against impossible value.
# https://staticcheck.dev/docs/checks/#SA4032
- SA4032
# Assignment to nil map.
# https://staticcheck.dev/docs/checks/#SA5000
- SA5000
# Deferring 'Close' before checking for a possible error.
# https://staticcheck.dev/docs/checks/#SA5001
- SA5001
# The empty for loop ("for {}") spins and can block the scheduler.
# https://staticcheck.dev/docs/checks/#SA5002
- SA5002
# Defers in infinite loops will never execute.
# https://staticcheck.dev/docs/checks/#SA5003
- SA5003
# "for { select { ..." with an empty default branch spins.
# https://staticcheck.dev/docs/checks/#SA5004
- SA5004
# The finalizer references the finalized object, preventing garbage collection.
# https://staticcheck.dev/docs/checks/#SA5005
- SA5005
# Infinite recursive call.
# https://staticcheck.dev/docs/checks/#SA5007
- SA5007
# Invalid struct tag.
# https://staticcheck.dev/docs/checks/#SA5008
- SA5008
# Invalid Printf call.
# https://staticcheck.dev/docs/checks/#SA5009
- SA5009
# Impossible type assertion.
# https://staticcheck.dev/docs/checks/#SA5010
- SA5010
# Possible nil pointer dereference.
# https://staticcheck.dev/docs/checks/#SA5011
- SA5011
# Passing odd-sized slice to function expecting even size.
# https://staticcheck.dev/docs/checks/#SA5012
- SA5012
# Using 'regexp.Match' or related in a loop, should use 'regexp.Compile'.
# https://staticcheck.dev/docs/checks/#SA6000
- SA6000
# Missing an optimization opportunity when indexing maps by byte slices.
# https://staticcheck.dev/docs/checks/#SA6001
- SA6001
# Storing non-pointer values in 'sync.Pool' allocates memory.
# https://staticcheck.dev/docs/checks/#SA6002
- SA6002
# Converting a string to a slice of runes before ranging over it.
# https://staticcheck.dev/docs/checks/#SA6003
- SA6003
# Inefficient string comparison with 'strings.ToLower' or 'strings.ToUpper'.
# https://staticcheck.dev/docs/checks/#SA6005
- SA6005
# Using io.WriteString to write '[]byte'.
# https://staticcheck.dev/docs/checks/#SA6006
- SA6006
# Defers in range loops may not run when you expect them to.
# https://staticcheck.dev/docs/checks/#SA9001
- SA9001
# Using a non-octal 'os.FileMode' that looks like it was meant to be in octal.
# https://staticcheck.dev/docs/checks/#SA9002
- SA9002
# Empty body in an if or else branch.
# https://staticcheck.dev/docs/checks/#SA9003
- SA9003
# Only the first constant has an explicit type.
# https://staticcheck.dev/docs/checks/#SA9004
- SA9004
# Trying to marshal a struct with no public fields nor custom marshaling.
# https://staticcheck.dev/docs/checks/#SA9005
- SA9005
# Dubious bit shifting of a fixed size integer value.
# https://staticcheck.dev/docs/checks/#SA9006
- SA9006
# Deleting a directory that shouldn't be deleted.
# https://staticcheck.dev/docs/checks/#SA9007
- SA9007
# 'else' branch of a type assertion is probably not reading the right value.
# https://staticcheck.dev/docs/checks/#SA9008
- SA9008
# Ineffectual Go compiler directive.
# https://staticcheck.dev/docs/checks/#SA9009
- SA9009
# Incorrect or missing package comment.
# https://staticcheck.dev/docs/checks/#ST1000
- ST1000
# Dot imports are discouraged.
# https://staticcheck.dev/docs/checks/#ST1001
- ST1001
# Poorly chosen identifier.
# https://staticcheck.dev/docs/checks/#ST1003
- ST1003
# Incorrectly formatted error string.
# https://staticcheck.dev/docs/checks/#ST1005
- ST1005
# Poorly chosen receiver name.
# https://staticcheck.dev/docs/checks/#ST1006
- ST1006
# A function's error value should be its last return value.
# https://staticcheck.dev/docs/checks/#ST1008
- ST1008
# Poorly chosen name for variable of type 'time.Duration'.
# https://staticcheck.dev/docs/checks/#ST1011
- ST1011
# Poorly chosen name for error variable.
# https://staticcheck.dev/docs/checks/#ST1012
- ST1012
# Should use constants for HTTP error codes, not magic numbers.
# https://staticcheck.dev/docs/checks/#ST1013
- ST1013
# A switch's default case should be the first or last case.
# https://staticcheck.dev/docs/checks/#ST1015
- ST1015
# Use consistent method receiver names.
# https://staticcheck.dev/docs/checks/#ST1016
- ST1016
# Don't use Yoda conditions.
# https://staticcheck.dev/docs/checks/#ST1017
- ST1017
# Avoid zero-width and control characters in string literals.
# https://staticcheck.dev/docs/checks/#ST1018
- ST1018
# Importing the same package multiple times.
# https://staticcheck.dev/docs/checks/#ST1019
- ST1019
# The documentation of an exported function should start with the function's name.
# https://staticcheck.dev/docs/checks/#ST1020
- ST1020
# The documentation of an exported type should start with type's name.
# https://staticcheck.dev/docs/checks/#ST1021
- ST1021
# The documentation of an exported variable or constant should start with variable's name.
# https://staticcheck.dev/docs/checks/#ST1022
- ST1022
# Redundant type in variable declaration.
# https://staticcheck.dev/docs/checks/#ST1023
- ST1023
# Use plain channel send or receive instead of single-case select.
# https://staticcheck.dev/docs/checks/#S1000
- S1000
# Replace for loop with call to copy.
# https://staticcheck.dev/docs/checks/#S1001
- S1001
# Omit comparison with boolean constant.
# https://staticcheck.dev/docs/checks/#S1002
- S1002
# Replace call to 'strings.Index' with 'strings.Contains'.
# https://staticcheck.dev/docs/checks/#S1003
- S1003
# Replace call to 'bytes.Compare' with 'bytes.Equal'.
# https://staticcheck.dev/docs/checks/#S1004
- S1004
# Drop unnecessary use of the blank identifier.
# https://staticcheck.dev/docs/checks/#S1005
- S1005
# Use "for { ... }" for infinite loops.
# https://staticcheck.dev/docs/checks/#S1006
- S1006
# Simplify regular expression by using raw string literal.
# https://staticcheck.dev/docs/checks/#S1007
- S1007
# Simplify returning boolean expression.
# https://staticcheck.dev/docs/checks/#S1008
- S1008
# Omit redundant nil check on slices, maps, and channels.
# https://staticcheck.dev/docs/checks/#S1009
- S1009
# Omit default slice index.
# https://staticcheck.dev/docs/checks/#S1010
- S1010
# Use a single 'append' to concatenate two slices.
# https://staticcheck.dev/docs/checks/#S1011
- S1011
# Replace 'time.Now().Sub(x)' with 'time.Since(x)'.
# https://staticcheck.dev/docs/checks/#S1012
- S1012
# Use a type conversion instead of manually copying struct fields.
# https://staticcheck.dev/docs/checks/#S1016
- S1016
# Replace manual trimming with 'strings.TrimPrefix'.
# https://staticcheck.dev/docs/checks/#S1017
- S1017
# Use "copy" for sliding elements.
# https://staticcheck.dev/docs/checks/#S1018
- S1018
# Simplify "make" call by omitting redundant arguments.
# https://staticcheck.dev/docs/checks/#S1019
- S1019
# Omit redundant nil check in type assertion.
# https://staticcheck.dev/docs/checks/#S1020
- S1020
# Merge variable declaration and assignment.
# https://staticcheck.dev/docs/checks/#S1021
- S1021
# Omit redundant control flow.
# https://staticcheck.dev/docs/checks/#S1023
- S1023
# Replace 'x.Sub(time.Now())' with 'time.Until(x)'.
# https://staticcheck.dev/docs/checks/#S1024
- S1024
# Don't use 'fmt.Sprintf("%s", x)' unnecessarily.
# https://staticcheck.dev/docs/checks/#S1025
- S1025
# Simplify error construction with 'fmt.Errorf'.
# https://staticcheck.dev/docs/checks/#S1028
- S1028
# Range over the string directly.
# https://staticcheck.dev/docs/checks/#S1029
- S1029
# Use 'bytes.Buffer.String' or 'bytes.Buffer.Bytes'.
# https://staticcheck.dev/docs/checks/#S1030
- S1030
# Omit redundant nil check around loop.
# https://staticcheck.dev/docs/checks/#S1031
- S1031
# Use 'sort.Ints(x)', 'sort.Float64s(x)', and 'sort.Strings(x)'.
# https://staticcheck.dev/docs/checks/#S1032
- S1032
# Unnecessary guard around call to "delete".
# https://staticcheck.dev/docs/checks/#S1033
- S1033
# Use result of type assertion to simplify cases.
# https://staticcheck.dev/docs/checks/#S1034
- S1034
# Redundant call to 'net/http.CanonicalHeaderKey' in method call on 'net/http.Header'.
# https://staticcheck.dev/docs/checks/#S1035
- S1035
# Unnecessary guard around map access.
# https://staticcheck.dev/docs/checks/#S1036
- S1036
# Elaborate way of sleeping.
# https://staticcheck.dev/docs/checks/#S1037
- S1037
# Unnecessarily complex way of printing formatted string.
# https://staticcheck.dev/docs/checks/#S1038
- S1038
# Unnecessary use of 'fmt.Sprint'.
# https://staticcheck.dev/docs/checks/#S1039
- S1039
# Type assertion to current type.
# https://staticcheck.dev/docs/checks/#S1040
- S1040
# Apply De Morgan's law.
# https://staticcheck.dev/docs/checks/#QF1001
- QF1001
# Convert untagged switch to tagged switch.
# https://staticcheck.dev/docs/checks/#QF1002
- QF1002
# Convert if/else-if chain to tagged switch.
# https://staticcheck.dev/docs/checks/#QF1003
- QF1003
# Use 'strings.ReplaceAll' instead of 'strings.Replace' with 'n == -1'.
# https://staticcheck.dev/docs/checks/#QF1004
- QF1004
# Expand call to 'math.Pow'.
# https://staticcheck.dev/docs/checks/#QF1005
- QF1005
# Lift 'if'+'break' into loop condition.
# https://staticcheck.dev/docs/checks/#QF1006
- QF1006
# Merge conditional assignment into variable declaration.
# https://staticcheck.dev/docs/checks/#QF1007
- QF1007
# Omit embedded fields from selector expression.
# https://staticcheck.dev/docs/checks/#QF1008
- QF1008
# Use 'time.Time.Equal' instead of '==' operator.
# https://staticcheck.dev/docs/checks/#QF1009
- QF1009
# Convert slice of bytes to string when printing it.
# https://staticcheck.dev/docs/checks/#QF1010
- QF1010
# Omit redundant type from variable declaration.
# https://staticcheck.dev/docs/checks/#QF1011
- QF1011
# Use 'fmt.Fprintf(x, ...)' instead of 'x.Write(fmt.Sprintf(...))'.
# https://staticcheck.dev/docs/checks/#QF1012
- QF1012
unconvert:
# Remove conversions that force intermediate rounding.
# Default: false
fast-math: true
# Be more conservative (experimental).
# Default: false
safe: true
usestdlibvars:
# Suggest the use of http.MethodXX.
# Default: true
http-method: false
# Suggest the use of http.StatusXX.
# Default: true
http-status-code: false
# Suggest the use of time.Month in time.Date.
# Default: false
time-date-month: true
# Suggest the use of time.Weekday.String().
# Default: true
time-weekday: true
# Suggest the use of time.Month.String().
# Default: false
time-month: true
# Suggest the use of time.Layout.
# Default: false
time-layout: true
# Suggest the use of crypto.Hash.String().
# Default: false
crypto-hash: true
# Suggest the use of rpc.DefaultXXPath.
# Default: false
default-rpc-path: true
# Suggest the use of sql.LevelXX.String().
# Default: false
sql-isolation-level: true
# Suggest the use of tls.SignatureScheme.String().
# Default: false
tls-signature-scheme: true
# Suggest the use of constant.Kind.String().
# Default: false
constant-kind: true
wsl_v5:
allow-first-in-block: true
allow-whole-block: true
branch-max-lines: 2
case-max-lines: 2
default: all
disable:
- if
- err
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -1,20 +1,109 @@
#!/usr/bin/env gmake -f #!/usr/bin/env gmake -f
GOOPTS=CGO_ENABLED=0 GOOPTS=CGO_ENABLED=0
BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false
MYNAME1=aleesa-telegram-go
BINARY1=${MYNAME1}
UNIX_BINARY1=${MYNAME1}
WINDOWS_BINARY1=${MYNAME1}.exe
MYNAME2=settings-migrator
BINARY2=${MYNAME2}
UNIX_BINARY2=${MYNAME2}
WINDOWS_BINARY2=${MYNAME2}.exe
RMCMD=rm -rf
# On windows binary name can depend not only on platform selected, but target too, we need no .exe suffix for linux bin.
ifeq ($(OS),Windows_NT)
ifdef GOOS
ifeq ($(GOOS),windows)
BINARY1=${WINDOWS_BINARY1}
BINARY2=${WINDOWS_BINARY2}
else # not ifeq ($(GOOS),windows)
BINARY1=${MYNAME1}
BINARY2=${MYNAME2}
endif # ifeq ($(GOOS),windows)
else # not ifdef GOOS
BINARY1=${WINDOWS_BINARY1}
BINARY2=${WINDOWS_BINARY2}
endif # ifdef GOOS
ifeq ($(SHELL), sh.exe)
RMCMD=DEL /Q /F
endif
endif
# Set newline symbol explicitly to aboud undefined behaviour in windows.
define IFS
endef
all: clean build all: clean build
build: build:
${GOOPTS} go build ${BUILDOPTS} -o aleesa-telegram-go ifeq ($(OS),Windows_NT)
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
ifeq ($(SHELL), sh.exe)
# # Vanilla cmd.exe / powershell.
SET "CGO_ENABLED=0"
go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1}
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # git-bash
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1}
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # Some other shell.
# # TODO: handle it.
$(info "-- Dunno how to handle this shell: ${SHELL}")
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
else # not ($(OS),Windows_NT)
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1}
endif # ifeq ($(OS),Windows_NT)
clean: clean:
go clean ifeq ($(OS),Windows_NT)
ifeq ($(SHELL),sh.exe)
# # Vanilla cmd.exe / powershell.
if exist ${WINDOWS_BINARY1} ${RMCMD} ${WINDOWS_BINARY1}
if exist ${UNIX_BINARY1} ${RMCMD} ${UNIX_BINARY1}
if exist ${WINDOWS_BINARY2} ${RMCMD} ${WINDOWS_BINARY2}
if exist ${UNIX_BINARY2} ${RMCMD} ${UNIX_BINARY2}
else # not ifeq ($(SHELL),sh.exe)
${RMCMD} ./${WINDOWS_BINARY1}
${RMCMD} ./${UNIX_BINARY1}
${RMCMD} ./${WINDOWS_BINARY2}
${RMCMD} ./${UNIX_BINARY2}
endif # ifeq ($(SHELL),sh.exe)
else # not ifeq ($(OS),Windows_NT)
${RMCMD} ./${UNIX_BINARY1}
${RMCMD} ./${UNIX_BINARY2}
endif
upgrade: upgrade:
rm -rf vendor go get -u ./...
go get -d -u -t ./...
go mod tidy go mod tidy
go mod vendor go mod vendor
# Build settings-migrator binary.
${MYNAME2}: clean
ifeq ($(OS),Windows_NT)
# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set.
ifeq ($(SHELL), sh.exe)
# # Vanilla cmd.exe / powershell.
SET "CGO_ENABLED=0"
go build ${BUILDOPTS} -o ${WINDOWS_BINARY2} ./cmd/${MYNAME2}
else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # git-bash
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${WINDOWS_BINARY2} ./cmd/${MYNAME2}
else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
# # Some other shell.
# # TODO: handle it.
$(info "-- Dunno how to handle this shell: ${SHELL}")
endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL))))
else # not ($(OS),Windows_NT)
CGO_ENABLED=0 go build ${BUILDOPTS} -o ${UNIX_BINARY2} ./cmd/${MYNAME2}
endif # ifeq ($(OS),Windows_NT)
# vim: set ft=make noet ai ts=4 sw=4 sts=4: # vim: set ft=make noet ai ts=4 sw=4 sts=4:

36
TODO.md
View File

@@ -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
View 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

View 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: */

View 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:

View 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: */

View File

@@ -1,31 +1,32 @@
{ {
# Cachedir used not only for caches but for some settings and stats too. # Data_dir used not only for caches but for some settings and stats too.
"cachedir" : "data/cache", "data_dir" : "data",
# Required for debugging reasons, if commented out or not exist, no debug logging appears at all. # Required for debugging reasons, if commented out or not exist, no debug logging appears at all.
"log" : "/var/log/aleesa-telegram/debug.log", "log" : "/var/log/aleesa-telegram/debug.log",
# Verbosity can be trace, debug, info (inform), notice, warning (warn), error (err), critical (crit, fatal), alert, emergency. # Verbosity can be trace, debug, info, warn, error.
# Currently implemented debug, info, notice, warn, error, fatal. Notice is default logging level. # Currently implemented debug, info, warn, error. Info is default logging level.
"loglevel": "notice", "loglevel": "info",
# Stuff for telegram itself. # Stuff for telegram itself.
"telegrambot" : { "telegram" : {
# Prefix-sign for all commands. In many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!". "token" : "Long_string_given_by_@BotFather"
# In telegram commands begin with "/" on the start of first (and only) line. In case of this bot you can define what to use. },
"csign" : "!",
"token" : "Long_string_given_by_@BotFather" # Prefix-sign for all commands. In many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!".
}, # In telegram commands begin with "/" on the start of first (and only) line. In case of this bot you can define what to use.
"csign" : "!",
# Unprivileged user to run this bot from. # Unprivileged user to run this bot from.
"user" : "nobody", "user" : "nobody",
"redis_server": "redis", "redis": {
"redis_port" : "6379", "server": "redis",
"redis_channels" : [ "port" : 6379,
"telegram" "channel" : "misc",
], "my_channel": "telegram"
}
"redis_router_channel" : "misc"
} }
// vim: set ft=json noet ai ts=4 sw=4 sts=4:

View File

@@ -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
View File

@@ -1,39 +1,42 @@
module aleesa-telegram-go module aleesa-telegram-go
go 1.19 go 1.25
require ( require (
github.com/NicoNex/echotron/v3 v3.29.0 github.com/NicoNex/echotron/v3 v3.33.1
github.com/cockroachdb/errors v1.11.1 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
github.com/cockroachdb/pebble v1.1.0 github.com/cockroachdb/errors v1.11.3
github.com/cockroachdb/pebble v1.1.1
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redis/v8 v8.11.5
github.com/hjson/hjson-go v3.3.0+incompatible github.com/hjson/hjson-go v3.3.0+incompatible
github.com/sirupsen/logrus v1.9.3
) )
require ( require (
github.com/DataDog/zstd v1.5.5 // indirect github.com/DataDog/zstd v1.5.5 // indirect
github.com/beorn7/perks v1.0.1 // 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/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // 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/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // 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/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
) )

94
go.sum
View File

@@ -1,32 +1,38 @@
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= 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.33.1 h1:P9bVvmnuVd0C7bR5q9TV6Pece/wBqZ6PSo0RqokZgng=
github.com/NicoNex/echotron/v3 v3.29.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= 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 h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= 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.1 h1:XnKU22oiCLy2Xn8vp1re67cXg4SAasg/WDt1NtcRFaw=
github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= 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 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= 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 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= 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 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 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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 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 h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI=
github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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/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.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 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 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 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/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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 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.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.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-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-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-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.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-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-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.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

177
internal/log/log.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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: */

View File

@@ -1,61 +1,62 @@
package main package tg
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"aleesa-telegram-go/internal/log"
"github.com/hjson/hjson-go" "github.com/hjson/hjson-go"
log "github.com/sirupsen/logrus"
) )
// parseConfig разбирает и валидирует даденный конфиг. // parseConfig разбирает и валидирует даденный конфиг.
func parseConfig(path string) (myConfig, error) { func parseConfig(path string) (MyConfig, error) {
fileInfo, err := os.Stat(path) fileInfo, err := os.Stat(path)
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём. // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
if err != nil { if err != nil {
return myConfig{}, err return MyConfig{}, err
} }
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата. // Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
if fileInfo.Size() > 65535 { if fileInfo.Size() > 65535 {
err := fmt.Errorf("config file %s is too long for config, skipping", path) err := fmt.Errorf("config file %s is too long for config, skipping", path)
return myConfig{}, err return MyConfig{}, err
} }
buf, err := os.ReadFile(path) buf, err := os.ReadFile(path)
// Не удалось прочитать. // Не удалось прочитать.
if err != nil { if err != nil {
return myConfig{}, err return MyConfig{}, err
} }
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется. // сериализуем в json, а потом json превращааем в структурку. Не очень эффективно, но он и не часто требуется.
var ( var (
sampleConfig myConfig sampleConfig MyConfig
tmp map[string]interface{} tmp map[string]any
) )
err = hjson.Unmarshal(buf, &tmp) err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить. // Не удалось распарсить.
if err != nil { if err != nil {
return myConfig{}, err return MyConfig{}, err
} }
tmpjson, err := json.Marshal(tmp) tmpjson, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json. // Не удалось преобразовать map-ку в json.
if err != nil { if err != nil {
return myConfig{}, err return MyConfig{}, err
} }
if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil { if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil {
return myConfig{}, err return MyConfig{}, err
} }
// Валидируем значения из конфига. // Валидируем значения из конфига.
@@ -72,29 +73,29 @@ func parseConfig(path string) (myConfig, error) {
if sampleConfig.Redis.Channel == "" { if sampleConfig.Redis.Channel == "" {
err := fmt.Errorf("channel field in config file %s must be set", path) err := fmt.Errorf("channel field in config file %s must be set", path)
return myConfig{}, err return MyConfig{}, err
} }
if sampleConfig.Redis.MyChannel == "" { if sampleConfig.Redis.MyChannel == "" {
err := fmt.Errorf("my_channel field in config file %s must be set", path) err := fmt.Errorf("my_channel field in config file %s must be set", path)
return myConfig{}, err return MyConfig{}, err
} }
if sampleConfig.Telegram.Token == "" { if sampleConfig.Telegram.Token == "" {
return myConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path) return MyConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path)
} }
if sampleConfig.Loglevel == "" { if sampleConfig.Loglevel == "" {
sampleConfig.Loglevel = "info" sampleConfig.Loglevel = "info"
} }
// sampleConfig.Log = "" if not set // sampleConfig.Log = "" if not set..
if sampleConfig.Csign == "" { if sampleConfig.Csign == "" {
err := fmt.Errorf("csign field in config file %s must be set", path) err := fmt.Errorf("csign field in config file %s must be set", path)
return myConfig{}, err return MyConfig{}, err
} }
if sampleConfig.ForwardsMax == 0 { if sampleConfig.ForwardsMax == 0 {
@@ -102,8 +103,10 @@ func parseConfig(path string) (myConfig, error) {
} }
if sampleConfig.DataDir == "" { if sampleConfig.DataDir == "" {
return myConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path) return MyConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
} }
return sampleConfig, err return sampleConfig, err
} }
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View 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: */

View 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: */

View 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
View 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
View 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: */

View 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
View 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
View File

@@ -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)
}
}

View File

@@ -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: */

View File

@@ -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: */

View File

@@ -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)
}
}

View File

@@ -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
View File

@@ -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: */

View File

@@ -1,4 +1,4 @@
| <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) [![PkgGoDev](https://pkg.go.dev/badge/github.com/NicoNex/echotron/v3)](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/NicoNex/echotron/v3)](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [![License](http://img.shields.io/badge/license-LGPL3.0-orange.svg?style=flat)](https://github.com/NicoNex/echotron/blob/master/LICENSE) [![Coverage Status](https://coveralls.io/repos/github/NicoNex/echotron/badge.svg?branch=master)](https://coveralls.io/github/NicoNex/echotron?branch=master) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![Telegram](https://img.shields.io/badge/Echotron%20News-blue?logo=telegram&style=flat)](https://t.me/echotronnews) | | <img src="assets/readme_banner.png" alt="logo" width="800"><br/><br/> [![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) [![PkgGoDev](https://pkg.go.dev/badge/github.com/NicoNex/echotron/v3)](https://pkg.go.dev/github.com/NicoNex/echotron/v3) [![Go Report Card](https://goreportcard.com/badge/github.com/NicoNex/echotron/v3)](https://goreportcard.com/report/github.com/NicoNex/echotron/v3) [![codecov](https://codecov.io/gh/NicoNex/echotron/graph/badge.svg?token=LVJGOEYL5M)](https://codecov.io/gh/NicoNex/echotron) [![License](http://img.shields.io/badge/license-LGPL3.0-orange.svg?style=flat)](https://github.com/NicoNex/echotron/blob/master/LICENSE) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![Telegram](https://img.shields.io/badge/Echotron%20News-blue?logo=telegram&style=flat)](https://t.me/echotronnews) |
| :------: | | :------: |
**Echotron** is an elegant and concurrent library for the Telegram bot API in Go. **Echotron** is an elegant and concurrent library for the Telegram bot API in Go.

View File

@@ -27,21 +27,32 @@ import (
// API is the object that contains all the functions that wrap those of the Telegram Bot API. // API is the object that contains all the functions that wrap those of the Telegram Bot API.
type API struct { type API struct {
token string client *client
base string token string
base string
} }
// NewAPI returns a new API object. // NewAPI returns a new API object.
func NewAPI(token string) API { func NewAPI(token string) API {
return API{ return API{
token: token, token: token,
base: fmt.Sprintf("https://api.telegram.org/bot%s/", 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. // GetUpdates is used to receive incoming updates using long polling.
func (a API) GetUpdates(opts *UpdateOptions) (res APIResponseUpdate, err error) { 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. // 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) addValues(vals, opts)
url = fmt.Sprintf("%s?%s", strings.TrimSuffix(url, "/"), vals.Encode()) 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 { if err != nil {
return return
} }
@@ -78,17 +89,17 @@ func (a API) DeleteWebhook(dropPendingUpdates bool) (res APIResponseBase, err er
var vals = make(url.Values) var vals = make(url.Values)
vals.Set("drop_pending_updates", btoa(dropPendingUpdates)) 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. // GetWebhookInfo is used to get current webhook status.
func (a API) GetWebhookInfo() (res APIResponseWebhook, err error) { 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. // GetMe is a simple method for testing your bot's auth token.
func (a API) GetMe() (res APIResponseUser, err error) { 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. // 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, // 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. // 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) { 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. // 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. // 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. // The method will return error 429 in the first 10 minutes after the bot is launched.
func (a API) Close() (res APIResponseBool, err error) { 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. // 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("text", text)
vals.Set("chat_id", itoa(chatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID)) vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_id", itoa(int64(messageID))) 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. // 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("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID)) vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_ids", string(msgIDs)) 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. // 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("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID)) vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_id", itoa(int64(messageID))) 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. // 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("chat_id", itoa(chatID))
vals.Set("from_chat_id", itoa(fromChatID)) vals.Set("from_chat_id", itoa(fromChatID))
vals.Set("message_ids", string(msgIDs)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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, // 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)) 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. // 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)) 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. // 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)) 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). // 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)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("latitude", ftoa(latitude)) vals.Set("latitude", ftoa(latitude))
vals.Set("longitude", ftoa(longitude)) 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. // 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("latitude", ftoa(latitude))
vals.Set("longitude", ftoa(longitude)) 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. // 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) { func (a API) StopMessageLiveLocation(msg MessageIDOptions, opts *StopLocationOptions) (res APIResponseMessage, err error) {
return get[APIResponseMessage](a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts)) return res, a.client.get(a.base, "stopMessageLiveLocation", addValues(urlValues(msg), opts), &res)
} }
// SendVenue is used to send information about a venue. // 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("longitude", ftoa(longitude))
vals.Set("title", title) vals.Set("title", title)
vals.Set("address", address) 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. // 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("chat_id", itoa(chatID))
vals.Set("phone_number", phoneNumber) vals.Set("phone_number", phoneNumber)
vals.Set("first_name", firstName) 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. // 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) var vals = make(url.Values)
pollOpts, err := json.Marshal(options) 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("chat_id", itoa(chatID))
vals.Set("question", question) vals.Set("question", question)
vals.Set("options", string(pollOpts)) 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. // 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("chat_id", itoa(chatID))
vals.Set("emoji", string(emoji)) 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. // 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("chat_id", itoa(chatID))
vals.Set("action", string(action)) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID))) 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. // 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) var vals = make(url.Values)
vals.Set("user_id", itoa(userID)) 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. // 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) var vals = make(url.Values)
vals.Set("file_id", fileID) 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. // 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. // 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. // When the download expires a new one can be requested by calling GetFile again.
func (a API) DownloadFile(filePath string) ([]byte, error) { 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", "https://api.telegram.org/file/bot%s/%s",
a.token, a.token,
filePath, filePath,
@@ -414,7 +425,7 @@ func (a API) BanChatMember(chatID, userID int64, opts *BanOptions) (res APIRespo
vals.Set("chat_id", itoa(chatID)) vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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) { func (a API) RestrictChatMember(chatID, userID int64, permissions ChatPermissions, opts *RestrictOptions) (res APIResponseBool, err error) {
var vals = make(url.Values) var vals = make(url.Values)
perm, err := serializePerms(permissions) perm, err := json.Marshal(permissions)
if err != nil { if err != nil {
return return
} }
vals.Set("chat_id", itoa(chatID)) vals.Set("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) vals.Set("user_id", itoa(userID))
vals.Set("permissions", perm) vals.Set("permissions", string(perm))
return get[APIResponseBool](a.base, "restrictChatMember", addValues(vals, opts)) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) vals.Set("user_id", itoa(userID))
vals.Set("custom_title", customTitle) 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. // 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("chat_id", itoa(chatID))
vals.Set("sender_chat_id", itoa(senderChatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("sender_chat_id", itoa(senderChatID)) 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. // 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) { func (a API) SetChatPermissions(chatID int64, permissions ChatPermissions, opts *ChatPermissionsOptions) (res APIResponseBool, err error) {
var vals = make(url.Values) var vals = make(url.Values)
perm, err := serializePerms(permissions) perm, err := json.Marshal(permissions)
if err != nil { if err != nil {
return return
} }
vals.Set("chat_id", itoa(chatID)) vals.Set("chat_id", itoa(chatID))
vals.Set("permissions", perm) vals.Set("permissions", string(perm))
return get[APIResponseBool](a.base, "setChatPermissions", addValues(vals, opts)) 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; // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("invite_link", inviteLink) 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. // 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("chat_id", itoa(chatID))
vals.Set("invite_link", inviteLink) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("title", title) 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. // 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("chat_id", itoa(chatID))
vals.Set("description", description) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID))) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID))) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("sticker_set_name", stickerSetName) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("name", name) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_thread_id", itoa(messageThreadID)) 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. // 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("chat_id", itoa(chatID))
vals.Set("name", name) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("callback_query_id", callbackID) 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. // 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("chat_id", itoa(chatID))
vals.Set("user_id", itoa(userID)) 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. // 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) jsn, _ := json.Marshal(commands)
vals.Set("commands", string(jsn)) 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. // 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) { 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. // 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) { 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. // 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("name", name)
vals.Set("language_code", languageCode) 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. // 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) var vals = make(url.Values)
vals.Set("language_code", languageCode) 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. // 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("description", description)
vals.Set("language_code", languageCode) 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. // 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) var vals = make(url.Values)
vals.Set("language_code", languageCode) 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, // 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("short_description", shortDescription)
vals.Set("language_code", languageCode) 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. // 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) var vals = make(url.Values)
vals.Set("language_code", languageCode) 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. // 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) var vals = make(url.Values)
vals.Set("text", text) 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. // EditMessageCaption is used to edit captions of messages.
func (a API) EditMessageCaption(msg MessageIDOptions, opts *MessageCaptionOptions) (res APIResponseMessage, err error) { 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. // 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. // 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. // 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. // 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) { func (a API) EditMessageMedia(msg MessageIDOptions, media InputMedia, opts *MessageMediaOptions) (res APIResponseMessage, err error) {
return postMedia[APIResponseMessage](a.base, "editMessageMedia", true, addValues(urlValues(msg), opts), media) 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. // EditMessageReplyMarkup is used to edit only the reply markup of messages.
func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkup) (res APIResponseMessage, err error) { func (a API) EditMessageReplyMarkup(msg MessageIDOptions, opts *MessageReplyMarkupOptions) (res APIResponseMessage, err error) {
return get[APIResponseMessage](a.base, "editMessageReplyMarkup", addValues(urlValues(msg), opts)) 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. // 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) var vals = make(url.Values)
vals.Set("chat_id", itoa(chatID)) vals.Set("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID))) 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: // 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("chat_id", itoa(chatID))
vals.Set("message_id", itoa(int64(messageID))) 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. // 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("chat_id", itoa(chatID))
vals.Set("message_ids", string(msgIDs)) vals.Set("message_ids", string(msgIDs))
return get[APIResponseBool](a.base, "deleteMessages", vals) return res, a.client.get(a.base, "deleteMessages", vals, &res)
} }

View File

@@ -54,11 +54,11 @@ type GetMyDefaultAdministratorRightsOptions struct {
// requested by the bot when it's added as an administrator to groups or channels. // 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 // These rights will be suggested to users, but they are are free to modify the list
// before adding the bot. // before adding the bot.
func (a API) SetMyDefaultAdministratorRights(opts SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) { func (a API) SetMyDefaultAdministratorRights(opts *SetMyDefaultAdministratorRightsOptions) (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "setMyDefaultAdministratorRights", urlValues(opts)) return res, a.client.get(a.base, "setMyDefaultAdministratorRights", urlValues(opts), &res)
} }
// GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot. // GetMyDefaultAdministratorRights is used to get the current default administrator rights of the bot.
func (a API) GetMyDefaultAdministratorRights(opts GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) { func (a API) GetMyDefaultAdministratorRights(opts *GetMyDefaultAdministratorRightsOptions) (res APIResponseChatAdministratorRights, err error) {
return get[APIResponseChatAdministratorRights](a.base, "getMyDefaultAdministratorRights", urlValues(opts)) return res, a.client.get(a.base, "getMyDefaultAdministratorRights", urlValues(opts), &res)
} }

View File

@@ -52,7 +52,7 @@ func (a API) SendGame(gameShortName string, chatID int64, opts *BaseOptions) (re
vals.Set("chat_id", itoa(chatID)) vals.Set("chat_id", itoa(chatID))
vals.Set("game_short_name", gameShortName) 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. // 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("user_id", itoa(userID))
vals.Set("score", itoa(int64(score))) 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. // 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) var vals = make(url.Values)
vals.Set("user_id", itoa(userID)) 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)
} }

View File

@@ -19,7 +19,6 @@
package echotron package echotron
import ( import (
"encoding/json"
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
@@ -27,6 +26,13 @@ import (
"strconv" "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 { func check(r APIResponse) error {
if b := r.Base(); !b.Ok { if b := r.Base(); !b.Ok {
return &APIError{code: b.ErrorCode, desc: b.Description} return &APIError{code: b.ErrorCode, desc: b.Description}
@@ -109,123 +115,6 @@ func readFile(im InputFile) (content []byte, path string, err error) {
return 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) { func toContent(ftype string, f InputFile) (content, error) {
if f.path != "" && len(f.content) == 0 { if f.path != "" && len(f.content) == 0 {
var err error var err error
@@ -247,87 +136,6 @@ func toInputMedia(media []GroupableInputMedia) (ret []InputMedia) {
return ret 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) { func joinURL(base, endpoint string, vals url.Values) (addr string, err error) {
addr, err = url.JoinPath(base, endpoint) addr, err = url.JoinPath(base, endpoint)
if err != nil { if err != nil {

View File

@@ -90,19 +90,20 @@ func (i InlineQueryResultArticle) ImplementsInlineQueryResult() {}
// By default, this photo will be sent by the user with optional caption. // By default, this photo will be sent by the user with optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
type InlineQueryResultPhoto struct { type InlineQueryResultPhoto struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
ThumbnailURL string `json:"thumbnail_url"` ThumbnailURL string `json:"thumbnail_url"`
PhotoURL string `json:"photo_url"` PhotoURL string `json:"photo_url"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"` ID string `json:"id"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
PhotoHeight int `json:"photo_height,omitempty"` PhotoHeight int `json:"photo_height,omitempty"`
PhotoWidth int `json:"photo_width,omitempty"` PhotoWidth int `json:"photo_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -112,20 +113,21 @@ func (i InlineQueryResultPhoto) ImplementsInlineQueryResult() {}
// By default, this animated GIF file will be sent by the user with optional caption. // By default, this animated GIF file will be sent by the user with optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
type InlineQueryResultGif struct { type InlineQueryResultGif struct {
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
GifURL string `json:"gif_url"` GifURL string `json:"gif_url"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ThumbnailURL string `json:"thumbnail_url"` ThumbnailURL string `json:"thumbnail_url"`
ID string `json:"id"` ID string `json:"id"`
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"` ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
GifDuration int `json:"gif_duration,omitempty"` GifDuration int `json:"gif_duration,omitempty"`
GifHeight int `json:"gif_height,omitempty"` GifHeight int `json:"gif_height,omitempty"`
GifWidth int `json:"gif_width,omitempty"` GifWidth int `json:"gif_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -135,20 +137,21 @@ func (i InlineQueryResultGif) ImplementsInlineQueryResult() {}
// By default, this animated MPEG-4 file will be sent by the user with optional caption. // By default, this animated MPEG-4 file will be sent by the user with optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
type InlineQueryResultMpeg4Gif struct { type InlineQueryResultMpeg4Gif struct {
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Mpeg4URL string `json:"mpeg4_url"` Mpeg4URL string `json:"mpeg4_url"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ThumbnailURL string `json:"thumbnail_url"` ThumbnailURL string `json:"thumbnail_url"`
ID string `json:"id"` ID string `json:"id"`
ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"` ThumbnailMimeType string `json:"thumbnail_mime_type,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Mpeg4Duration int `json:"mpeg4_duration,omitempty"` Mpeg4Duration int `json:"mpeg4_duration,omitempty"`
Mpeg4Height int `json:"mpeg4_height,omitempty"` Mpeg4Height int `json:"mpeg4_height,omitempty"`
Mpeg4Width int `json:"mpeg4_width,omitempty"` Mpeg4Width int `json:"mpeg4_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -158,21 +161,22 @@ func (i InlineQueryResultMpeg4Gif) ImplementsInlineQueryResult() {}
// By default, this video file will be sent by the user with an optional caption. // By default, this video file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
type InlineQueryResultVideo struct { type InlineQueryResultVideo struct {
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
MimeType string `json:"mime_type"` MimeType string `json:"mime_type"`
ThumbnailURL string `json:"thumbnail_url"` ThumbnailURL string `json:"thumbnail_url"`
Title string `json:"title"` Title string `json:"title"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ID string `json:"id"` ID string `json:"id"`
VideoURL string `json:"video_url"` VideoURL string `json:"video_url"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
VideoHeight int `json:"video_height,omitempty"` VideoHeight int `json:"video_height,omitempty"`
VideoDuration int `json:"video_duration,omitempty"` VideoDuration int `json:"video_duration,omitempty"`
VideoWidth int `json:"video_width,omitempty"` VideoWidth int `json:"video_width,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -323,16 +327,17 @@ func (i InlineQueryResultGame) ImplementsInlineQueryResult() {}
// By default, this photo will be sent by the user with an optional caption. // By default, this photo will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the photo.
type InlineQueryResultCachedPhoto struct { type InlineQueryResultCachedPhoto struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
ID string `json:"id"` ID string `json:"id"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
PhotoFileID string `json:"photo_file_id"` PhotoFileID string `json:"photo_file_id"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -342,15 +347,16 @@ func (i InlineQueryResultCachedPhoto) ImplementsInlineQueryResult() {}
// By default, this animated GIF file will be sent by the user with an optional caption. // By default, this animated GIF file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with specified content instead of the animation. // Alternatively, you can use InputMessageContent to send a message with specified content instead of the animation.
type InlineQueryResultCachedGif struct { type InlineQueryResultCachedGif struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"` ID string `json:"id"`
GifFileID string `json:"gif_file_id"` GifFileID string `json:"gif_file_id"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -360,15 +366,16 @@ func (i InlineQueryResultCachedGif) ImplementsInlineQueryResult() {}
// By default, this animated MPEG-4 file will be sent by the user with an optional caption. // By default, this animated MPEG-4 file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the animation.
type InlineQueryResultCachedMpeg4Gif struct { type InlineQueryResultCachedMpeg4Gif struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
ID string `json:"id"` ID string `json:"id"`
Mpeg4FileID string `json:"mpeg4_file_id"` Mpeg4FileID string `json:"mpeg4_file_id"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // ImplementsInlineQueryResult is used to implement the InlineQueryResult interface.
@@ -411,16 +418,17 @@ func (i InlineQueryResultCachedDocument) ImplementsInlineQueryResult() {}
// By default, this video file will be sent by the user with an optional caption. // By default, this video file will be sent by the user with an optional caption.
// Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video. // Alternatively, you can use InputMessageContent to send a message with the specified content instead of the video.
type InlineQueryResultCachedVideo struct { type InlineQueryResultCachedVideo struct {
ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"`
InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` InputMessageContent InputMessageContent `json:"input_message_content,omitempty"`
Type InlineQueryType `json:"type"` Type InlineQueryType `json:"type"`
ID string `json:"id"` ID string `json:"id"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
VideoFileID string `json:"video_file_id"` VideoFileID string `json:"video_file_id"`
Title string `json:"title"` Title string `json:"title"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
ShowCaptionAboveMedia bool `json:"show_caption_above_media,omitempty"`
} }
// ImplementsInlineQueryResult is used to implement the InlineQueryResult interface. // 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. // InputTextMessageContent represents the content of a text message to be sent as the result of an inline query.
type InputTextMessageContent struct { type InputTextMessageContent struct {
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
MessageText string `json:"message_text"` MessageText string `json:"message_text"`
ParseMode string `json:"parse_mode,omitempty"` ParseMode string `json:"parse_mode,omitempty"`
Entities []*MessageEntity `json:"entities,omitempty"` Entities []*MessageEntity `json:"entities,omitempty"`
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
} }
// ImplementsInputMessageContent is used to implement the InputMessageContent interface. // ImplementsInputMessageContent is used to implement the InputMessageContent interface.
@@ -516,6 +524,33 @@ type InputContactMessageContent struct {
// ImplementsInputMessageContent is used to implement the InputMessageContent interface. // ImplementsInputMessageContent is used to implement the InputMessageContent interface.
func (i InputContactMessageContent) ImplementsInputMessageContent() {} 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. // InlineQueryResultsButton represents a button to be shown above inline query results.
// You MUST use exactly one of the fields. // You MUST use exactly one of the fields.
type InlineQueryResultsButton struct { type InlineQueryResultsButton struct {
@@ -539,5 +574,5 @@ func (a API) AnswerInlineQuery(inlineQueryID string, results []InlineQueryResult
jsn, _ := json.Marshal(results) jsn, _ := json.Marshal(results)
vals.Set("inline_query_id", inlineQueryID) vals.Set("inline_query_id", inlineQueryID)
vals.Set("results", string(jsn)) 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)
} }

View File

@@ -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. // 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) { func (a API) SetChatMenuButton(opts *SetChatMenuButtonOptions) (res APIResponseBool, err error) {
return get[APIResponseBool](a.base, "setChatMenuButton", urlValues(opts)) 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. // 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) { func (a API) GetChatMenuButton(opts *GetChatMenuButtonOptions) (res APIResponseMenuButton, err error) {
return get[APIResponseMenuButton](a.base, "getChatMenuButton", urlValues(opts)) return res, a.client.get(a.base, "getChatMenuButton", urlValues(opts), &res)
} }

View File

@@ -20,99 +20,345 @@ package echotron
import ( import (
"bytes" "bytes"
"context"
"encoding/json"
"fmt"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"time"
"golang.org/x/time/rate"
) )
// content is a struct which contains a file's name, its type and its data. type client struct {
type content struct { *http.Client
fname string *sync.RWMutex
ftype string cl map[string]*rate.Limiter // chat based limiter
fdata []byte gl *rate.Limiter // global limiter
climiter func() *rate.Limiter
} }
// sendGetRequest is used to send an HTTP GET request. var lclient = newClient()
func sendGetRequest(url string) ([]byte, error) {
resp, err := http.Get(url) // 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 { if err != nil {
return []byte{}, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return []byte{}, err return nil, err
} }
return data, nil return data, nil
} }
// sendPostRequest is used to send an HTTP POST request. func (c client) doPost(reqURL string, files ...content) ([]byte, error) {
func sendPostRequest(url string, files ...content) ([]byte, error) { var (
var buf = new(bytes.Buffer) buf = new(bytes.Buffer)
var w = multipart.NewWriter(buf) w = multipart.NewWriter(buf)
)
for _, f := range files { for _, f := range files {
part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname)) part, err := w.CreateFormFile(f.ftype, filepath.Base(f.fname))
if err != nil { if err != nil {
return []byte{}, err return nil, err
} }
part.Write(f.fdata) part.Write(f.fdata)
} }
w.Close() w.Close()
req, err := http.NewRequest("POST", url, buf) req, err := http.NewRequest(http.MethodPost, reqURL, buf)
if err != nil { if err != nil {
return []byte{}, err return nil, err
} }
req.Header.Add("Content-Type", w.FormDataContentType()) req.Header.Add("Content-Type", w.FormDataContentType())
var client = new(http.Client) res, err := c.Do(req)
res, err := client.Do(req)
if err != nil { if err != nil {
return []byte{}, err return nil, err
} }
defer res.Body.Close() defer res.Body.Close()
return io.ReadAll(res.Body)
cnt, err := io.ReadAll(res.Body)
if err != nil {
return []byte{}, err
}
return cnt, nil
} }
// sendPostForm is used to send an "application/x-www-form-urlencoded" through an HTTP POST request. func (c client) doPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
func sendPostForm(reqURL string, keyVals map[string]string) ([]byte, error) {
var form = make(url.Values) var form = make(url.Values)
for k, v := range keyVals { for k, v := range keyVals {
form.Add(k, v) 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 { if err != nil {
return []byte{}, err return nil, err
} }
request.PostForm = form req.PostForm = form
request.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
var client http.Client res, err := c.Do(req)
response, err := client.Do(request)
if err != nil { if err != nil {
return []byte{}, err return nil, err
} }
defer response.Body.Close() defer res.Body.Close()
return io.ReadAll(res.Body)
content, err := io.ReadAll(response.Body) }
if err != nil {
return []byte{}, err func (c client) sendFile(file, thumbnail InputFile, url, fileType string) (res []byte, err error) {
} var cnt []content
return content, nil 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)
} }

View File

@@ -74,23 +74,25 @@ type MessageEntityType string
// These are all the possible types for MessageEntityType. // These are all the possible types for MessageEntityType.
const ( const (
MentionEntity MessageEntityType = "mention" MentionEntity MessageEntityType = "mention"
HashtagEntity = "hashtag" HashtagEntity = "hashtag"
CashtagEntity = "cashtag" CashtagEntity = "cashtag"
BotCommandEntity = "bot_command" BotCommandEntity = "bot_command"
UrlEntity = "url" UrlEntity = "url"
EmailEntity = "email" EmailEntity = "email"
PhoneNumberEntity = "phone_number" PhoneNumberEntity = "phone_number"
BoldEntity = "bold" BoldEntity = "bold"
ItalicEntity = "italic" ItalicEntity = "italic"
UnderlineEntity = "underline" UnderlineEntity = "underline"
StrikethroughEntity = "strikethrough" StrikethroughEntity = "strikethrough"
SpoilerEntity = "spoiler" SpoilerEntity = "spoiler"
CodeEntity = "code" BlockQuoteEntity = "blockquote"
PreEntity = "pre" ExpandableBlockQuoteEntity = "expandable_blockquote"
TextLinkEntity = "text_link" CodeEntity = "code"
TextMentionEntity = "text_mention" PreEntity = "pre"
CustomEmojiEntity = "custom_emoji" TextLinkEntity = "text_link"
TextMentionEntity = "text_mention"
CustomEmojiEntity = "custom_emoji"
) )
// UpdateType is a custom type for the various update types that a bot can be subscribed to. // UpdateType is a custom type for the various update types that a bot can be subscribed to.
@@ -137,10 +139,13 @@ type KeyboardButtonPollType struct {
// KeyboardButtonRequestUsers defines the criteria used to request suitable users. // KeyboardButtonRequestUsers defines the criteria used to request suitable users.
// The identifiers of the selected users will be shared with the bot when the corresponding button is pressed. // The identifiers of the selected users will be shared with the bot when the corresponding button is pressed.
type KeyboardButtonRequestUsers struct { type KeyboardButtonRequestUsers struct {
RequestID int `json:"request_id"` RequestID int `json:"request_id"`
MaxQuantity int `json:"max_quantity,omitempty"` MaxQuantity int `json:"max_quantity,omitempty"`
UserIsBot bool `json:"user_is_bot,omitempty"` UserIsBot bool `json:"user_is_bot,omitempty"`
UserIsPremium bool `json:"user_is_premium,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. // KeyboardButtonRequestChat defines the criteria used to request a suitable chat.
@@ -154,6 +159,9 @@ type KeyboardButtonRequestChat struct {
ChatHasUsername bool `json:"chat_has_username,omitempty"` ChatHasUsername bool `json:"chat_has_username,omitempty"`
ChatIsCreated bool `json:"chat_is_created,omitempty"` ChatIsCreated bool `json:"chat_is_created,omitempty"`
BotIsMember bool `json:"bot_is_member,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. // ReplyKeyboardMarkup represents a custom keyboard with reply options.
@@ -233,23 +241,27 @@ type WebhookOptions struct {
// BaseOptions contains the optional parameters used frequently in some Telegram API methods. // BaseOptions contains the optional parameters used frequently in some Telegram API methods.
type BaseOptions struct { type BaseOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` BusinessConnectionID string `query:"business_connection_id"`
ReplyParameters ReplyParameters `query:"reply_parameters"` MessageEffectID string `query:"message_effect_id"`
MessageThreadID int `query:"message_thread_id"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
DisableNotification bool `query:"disable_notification"` ReplyParameters ReplyParameters `query:"reply_parameters"`
ProtectContent bool `query:"protect_content"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// MessageOptions contains the optional parameters used by some Telegram API methods. // MessageOptions contains the optional parameters used by some Telegram API methods.
type MessageOptions struct { type MessageOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"` ParseMode ParseMode `query:"parse_mode"`
Entities []MessageEntity `query:"entities"` LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
MessageThreadID int64 `query:"message_thread_id"` Entities []MessageEntity `query:"entities"`
DisableNotification bool `query:"disable_notification"` ReplyParameters ReplyParameters `query:"reply_parameters"`
ProtectContent bool `query:"protect_content"` MessageThreadID int64 `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// PinMessageOptions contains the optional parameters used by the PinChatMember method. // PinMessageOptions contains the optional parameters used by the PinChatMember method.
@@ -266,14 +278,15 @@ type ForwardOptions struct {
// CopyOptions contains the optional parameters used by the CopyMessage method. // CopyOptions contains the optional parameters used by the CopyMessage method.
type CopyOptions struct { type CopyOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` ParseMode ParseMode `query:"parse_mode"`
ParseMode ParseMode `query:"parse_mode"` Caption string `query:"caption"`
Caption string `query:"caption"` CaptionEntities []MessageEntity `query:"caption_entities"`
CaptionEntities []MessageEntity `query:"caption_entities"` ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"` DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"` ProtectContent bool `query:"protect_content"`
ShowCaptionAboveMedia bool `query:"show_caption_above_media"`
} }
// CopyMessagesOptions contains the optional parameters used by the CopyMessages methods. // CopyMessagesOptions contains the optional parameters used by the CopyMessages methods.
@@ -286,12 +299,14 @@ type CopyMessagesOptions struct {
// StickerOptions contains the optional parameters used by the SendSticker method. // StickerOptions contains the optional parameters used by the SendSticker method.
type StickerOptions struct { type StickerOptions struct {
Emoji string `query:"emoji"` BusinessConnectionID string `query:"business_connection_id"`
ReplyMarkup ReplyMarkup `query:"reply_markup"` Emoji string `query:"emoji"`
ReplyParameters ReplyParameters `query:"reply_parameters"` MessageEffectID string `query:"message_effect_id"`
MessageThreadID int `query:"message_thread_id"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
DisableNotification bool `query:"disable_notification"` ReplyParameters ReplyParameters `query:"reply_parameters"`
ProtectContent bool `query:"protect_content"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// InputFile is a struct which contains data about a file to be sent. // InputFile is a struct which contains data about a file to be sent.
@@ -299,7 +314,6 @@ type InputFile struct {
id string id string
path string path string
url string url string
ftype string
content []byte content []byte
} }
@@ -325,41 +339,48 @@ func NewInputFileBytes(fileName string, content []byte) InputFile {
// PhotoOptions contains the optional parameters used by the SendPhoto method. // PhotoOptions contains the optional parameters used by the SendPhoto method.
type PhotoOptions struct { type PhotoOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
Caption string `query:"caption"` ParseMode ParseMode `query:"parse_mode"`
CaptionEntities []MessageEntity `query:"caption_entities"` Caption string `query:"caption"`
MessageThreadID int `query:"message_thread_id"` CaptionEntities []MessageEntity `query:"caption_entities"`
HasSpoiler bool `query:"has_spoiler"` ReplyParameters ReplyParameters `query:"reply_parameters"`
DisableNotification bool `query:"disable_notification"` MessageThreadID int `query:"message_thread_id"`
ProtectContent bool `query:"protect_content"` 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. // AudioOptions contains the optional parameters used by the SendAudio method.
type AudioOptions struct { type AudioOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` Title string `query:"title"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
Caption string `query:"caption"` ParseMode ParseMode `query:"parse_mode"`
Performer string `query:"performer"` Caption string `query:"caption"`
Title string `query:"title"` Performer string `query:"performer"`
Thumbnail InputFile BusinessConnectionID string `query:"business_connection_id"`
CaptionEntities []MessageEntity `query:"caption_entities"` Thumbnail InputFile
MessageThreadID int `query:"message_thread_id"` CaptionEntities []MessageEntity `query:"caption_entities"`
Duration int `query:"duration"` ReplyParameters ReplyParameters `query:"reply_parameters"`
DisableNotification bool `query:"disable_notification"` MessageThreadID int `query:"message_thread_id"`
ProtectContent bool `query:"protect_content"` Duration int `query:"duration"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// DocumentOptions contains the optional parameters used by the SendDocument method. // DocumentOptions contains the optional parameters used by the SendDocument method.
type DocumentOptions struct { type DocumentOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
Caption string `query:"caption"` ParseMode ParseMode `query:"parse_mode"`
Caption string `query:"caption"`
Thumbnail InputFile Thumbnail InputFile
CaptionEntities []MessageEntity `query:"caption_entities"` CaptionEntities []MessageEntity `query:"caption_entities"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
MessageThreadID int `query:"message_thread_id"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"` DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"` ProtectContent bool `query:"protect_content"`
@@ -368,74 +389,91 @@ type DocumentOptions struct {
// VideoOptions contains the optional parameters used by the SendVideo method. // VideoOptions contains the optional parameters used by the SendVideo method.
type VideoOptions struct { type VideoOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
Caption string `query:"caption"` Caption string `query:"caption"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
Thumbnail InputFile ParseMode ParseMode `query:"parse_mode"`
CaptionEntities []MessageEntity `query:"caption_entities"` Thumbnail InputFile
MessageThreadID int `query:"message_thread_id"` CaptionEntities []MessageEntity `query:"caption_entities"`
Duration int `query:"duration"` ReplyParameters ReplyParameters `query:"reply_parameters"`
Width int `query:"width"` MessageThreadID int `query:"message_thread_id"`
Height int `query:"height"` Duration int `query:"duration"`
HasSpoiler bool `query:"has_spoiler"` Width int `query:"width"`
SupportsStreaming bool `query:"supports_streaming"` Height int `query:"height"`
DisableNotification bool `query:"disable_notification"` HasSpoiler bool `query:"has_spoiler"`
ProtectContent bool `query:"protect_content"` 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. // AnimationOptions contains the optional parameters used by the SendAnimation method.
type AnimationOptions struct { type AnimationOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
Caption string `query:"caption"` ParseMode ParseMode `query:"parse_mode"`
Thumbnail InputFile Caption string `query:"caption"`
CaptionEntities []MessageEntity `query:"caption_entities"` Thumbnail InputFile
MessageThreadID int `query:"message_thread_id"` CaptionEntities []MessageEntity `query:"caption_entities"`
Duration int `query:"duration"` ReplyParameters ReplyParameters `query:"reply_parameters"`
Width int `query:"width"` MessageThreadID int `query:"message_thread_id"`
Height int `query:"height"` Duration int `query:"duration"`
HasSpoiler bool `query:"has_spoiler"` Width int `query:"width"`
DisableNotification bool `query:"disable_notification"` Height int `query:"height"`
ProtectContent bool `query:"protect_content"` 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. // VoiceOptions contains the optional parameters used by the SendVoice method.
type VoiceOptions struct { type VoiceOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
ParseMode ParseMode `query:"parse_mode"` MessageEffectID string `query:"message_effect_id"`
Caption string `query:"caption"` ParseMode ParseMode `query:"parse_mode"`
CaptionEntities []MessageEntity `query:"caption_entities"` Caption string `query:"caption"`
MessageThreadID int `query:"message_thread_id"` CaptionEntities []MessageEntity `query:"caption_entities"`
Duration int `query:"duration"` ReplyParameters ReplyParameters `query:"reply_parameters"`
DisableNotification bool `query:"disable_notification"` MessageThreadID int `query:"message_thread_id"`
ProtectContent bool `query:"protect_content"` Duration int `query:"duration"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// VideoNoteOptions contains the optional parameters used by the SendVideoNote method. // VideoNoteOptions contains the optional parameters used by the SendVideoNote method.
type VideoNoteOptions struct { type VideoNoteOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
Thumbnail InputFile MessageEffectID string `query:"message_effect_id"`
MessageThreadID int `query:"message_thread_id"` Thumbnail InputFile
Duration int `query:"duration"` ReplyParameters ReplyParameters `query:"reply_parameters"`
Length int `query:"length"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"` Duration int `query:"duration"`
ProtectContent bool `query:"protect_content"` Length int `query:"length"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// MediaGroupOptions contains the optional parameters used by the SendMediaGroup method. // MediaGroupOptions contains the optional parameters used by the SendMediaGroup method.
type MediaGroupOptions struct { type MediaGroupOptions struct {
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
MessageThreadID int `query:"message_thread_id"` MessageEffectID string `query:"message_effect_id"`
DisableNotification bool `query:"disable_notification"` ReplyParameters ReplyParameters `query:"reply_parameters"`
ProtectContent bool `query:"protect_content"` 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. // LocationOptions contains the optional parameters used by the SendLocation method.
type LocationOptions struct { type LocationOptions struct {
BusinessConnectionID string `query:"business_connection_id"`
MessageEffectID string `query:"message_effect_id"`
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` ReplyParameters ReplyParameters `query:"reply_parameters"`
HorizontalAccuracy float64 `query:"horizontal_accuracy"` HorizontalAccuracy float64 `query:"horizontal_accuracy"`
@@ -449,46 +487,62 @@ type LocationOptions struct {
// EditLocationOptions contains the optional parameters used by the EditMessageLiveLocation method. // EditLocationOptions contains the optional parameters used by the EditMessageLiveLocation method.
type EditLocationOptions struct { type EditLocationOptions struct {
BusinessConnectionID string `query:"business_connection_id"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"` ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
HorizontalAccuracy float64 `query:"horizontal_accuracy"` HorizontalAccuracy float64 `query:"horizontal_accuracy"`
Heading int `query:"heading"` Heading int `query:"heading"`
LivePeriod int `query:"live_period"`
ProximityAlertRadius int `query:"proximity_alert_radius"` 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. // VenueOptions contains the optional parameters used by the SendVenue method.
type VenueOptions struct { type VenueOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
FoursquareID string `query:"foursquare_id"` FoursquareID string `query:"foursquare_id"`
FoursquareType string `query:"foursquare_type"` FoursquareType string `query:"foursquare_type"`
GooglePlaceType string `query:"google_place_type"` GooglePlaceType string `query:"google_place_type"`
GooglePlaceID string `query:"google_place_id"` GooglePlaceID string `query:"google_place_id"`
MessageThreadID int `query:"message_thread_id"` MessageEffectID string `query:"message_effect_id"`
DisableNotification bool `query:"disable_notification"` ReplyParameters ReplyParameters `query:"reply_parameters"`
ProtectContent bool `query:"protect_content"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// ContactOptions contains the optional parameters used by the SendContact method. // ContactOptions contains the optional parameters used by the SendContact method.
type ContactOptions struct { type ContactOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
VCard string `query:"vcard"` VCard string `query:"vcard"`
LastName string `query:"last_name"` LastName string `query:"last_name"`
MessageThreadID int `query:"message_thread_id"` MessageEffectID string `query:"message_effect_id"`
DisableNotification bool `query:"disable_notification"` ReplyParameters ReplyParameters `query:"reply_parameters"`
ProtectContent bool `query:"protect_content"` MessageThreadID int `query:"message_thread_id"`
DisableNotification bool `query:"disable_notification"`
ProtectContent bool `query:"protect_content"`
} }
// PollOptions contains the optional parameters used by the SendPoll method. // PollOptions contains the optional parameters used by the SendPoll method.
type PollOptions struct { type PollOptions struct {
ReplyMarkup ReplyMarkup `query:"reply_markup"` ReplyMarkup ReplyMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"` BusinessConnectionID string `query:"business_connection_id"`
Explanation string `query:"explanation"` Explanation string `query:"explanation"`
MessageEffectID string `query:"message_effect_id"`
ExplanationParseMode ParseMode `query:"explanation_parse_mode"` ExplanationParseMode ParseMode `query:"explanation_parse_mode"`
QuestionParseMode ParseMode `query:"question_parse_mode"`
Type PollType `query:"type"` Type PollType `query:"type"`
ExplanationEntities []MessageEntity `query:"explanation_entities"` 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"` CorrectOptionID int `query:"correct_option_id"`
MessageThreadID int `query:"message_thread_id"`
CloseDate int `query:"close_date"` CloseDate int `query:"close_date"`
OpenPeriod int `query:"open_period"` OpenPeriod int `query:"open_period"`
IsClosed bool `query:"is_closed"` IsClosed bool `query:"is_closed"`
@@ -498,6 +552,12 @@ type PollOptions struct {
IsAnonymous bool `query:"is_anonymous"` 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. // BanOptions contains the optional parameters used by the BanChatMember method.
type BanOptions struct { type BanOptions struct {
UntilDate int `query:"until_date"` UntilDate int `query:"until_date"`
@@ -580,23 +640,33 @@ func NewInlineMessageID(ID string) MessageIDOptions {
// MessageTextOptions contains the optional parameters used by the EditMessageText method. // MessageTextOptions contains the optional parameters used by the EditMessageText method.
type MessageTextOptions struct { type MessageTextOptions struct {
ParseMode ParseMode `query:"parse_mode"` ParseMode ParseMode `query:"parse_mode"`
Entities []MessageEntity `query:"entities"` BusinessConnectionID string `query:"business_connection_id"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"` Entities []MessageEntity `query:"entities"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"` ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
LinkPreviewOptions LinkPreviewOptions `query:"link_preview_options"`
} }
// MessageCaptionOptions contains the optional parameters used by the EditMessageCaption method. // MessageCaptionOptions contains the optional parameters used by the EditMessageCaption method.
type MessageCaptionOptions struct { type MessageCaptionOptions struct {
Caption string `query:"caption"` Caption string `query:"caption"`
ParseMode ParseMode `query:"parse_mode"` BusinessConnectionID string `query:"business_connection_id"`
CaptionEntities []MessageEntity `query:"caption_entities"` ParseMode ParseMode `query:"parse_mode"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"` 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. // MessageMediaOptions contains the optional parameters used by the EditMessageMedia method.
type MessageReplyMarkup struct { type MessageMediaOptions struct {
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"` 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"`
} }
// CommandOptions contains the optional parameters used by the SetMyCommands, DeleteMyCommands and GetMyCommands methods. // CommandOptions contains the optional parameters used by the SetMyCommands, DeleteMyCommands and GetMyCommands methods.
@@ -610,17 +680,19 @@ type InvoiceOptions struct {
StartParameter string `query:"start_parameter"` StartParameter string `query:"start_parameter"`
ProviderData string `query:"provider_data"` ProviderData string `query:"provider_data"`
PhotoURL string `query:"photo_url"` PhotoURL string `query:"photo_url"`
ProviderToken string `query:"provider_token"`
MessageEffectID string `query:"message_effect_id"`
ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"` ReplyMarkup InlineKeyboardMarkup `query:"reply_markup"`
ReplyParameters ReplyParameters `query:"reply_parameters"`
SuggestedTipAmount []int `query:"suggested_tip_amounts"` 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"` PhotoSize int `query:"photo_size"`
PhotoWidth int `query:"photo_width"` PhotoWidth int `query:"photo_width"`
PhotoHeight int `query:"photo_height"` PhotoHeight int `query:"photo_height"`
MaxTipAmount int `query:"max_tip_amount"` MessageThreadID int `query:"message_thread_id"`
NeedPhoneNumber bool `query:"need_phone_number"`
NeepShippingAddress bool `query:"need_shipping_address"`
SendPhoneNumberToProvider bool `query:"send_phone_number_to_provider"` 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"` SendEmailToProvider bool `query:"send_email_to_provider"`
IsFlexible bool `query:"is_flexible"` IsFlexible bool `query:"is_flexible"`
DisableNotification bool `query:"disable_notification"` DisableNotification bool `query:"disable_notification"`
@@ -633,6 +705,7 @@ type InvoiceOptions struct {
type CreateInvoiceLinkOptions struct { type CreateInvoiceLinkOptions struct {
ProviderData string `query:"provider_data"` ProviderData string `query:"provider_data"`
PhotoURL string `query:"photo_url"` PhotoURL string `query:"photo_url"`
ProviderToken string `query:"provider_token"`
SuggestedTipAmounts []int `query:"suggested_tip_amounts"` SuggestedTipAmounts []int `query:"suggested_tip_amounts"`
PhotoSize int `query:"photo_size"` PhotoSize int `query:"photo_size"`
PhotoWidth int `query:"photo_width"` PhotoWidth int `query:"photo_width"`
@@ -679,7 +752,8 @@ type EditTopicOptions struct {
// ChatActionOptions contains the optional parameters used by the SendChatAction API method. // ChatActionOptions contains the optional parameters used by the SendChatAction API method.
type ChatActionOptions struct { type ChatActionOptions struct {
MessageThreadID int `query:"message_thread_id"` BusinessConnectionID string `query:"business_connection_id"`
MessageThreadID int `query:"message_thread_id"`
} }
// MessageReactionOptions contains the optional parameters used by the SetMessageReaction API method. // MessageReactionOptions contains the optional parameters used by the SetMessageReaction API method.

View File

@@ -224,5 +224,5 @@ func (a API) SetPassportDataErrors(userID int64, errors []PassportElementError)
vals.Set("user_id", itoa(userID)) vals.Set("user_id", itoa(userID))
vals.Set("errors", string(errorsArr)) vals.Set("errors", string(errorsArr))
return get[APIResponseBool](a.base, "setPassportDataErrors", vals) return res, a.client.get(a.base, "setPassportDataErrors", vals, &res)
} }

View File

@@ -96,8 +96,90 @@ type PreCheckoutQuery struct {
TotalAmount int `json:"total_amount"` 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. // 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) var vals = make(url.Values)
p, err := json.Marshal(prices) 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("title", title)
vals.Set("description", description) vals.Set("description", description)
vals.Set("payload", payload) vals.Set("payload", payload)
vals.Set("provider_token", providerToken)
vals.Set("currency", currency) vals.Set("currency", currency)
vals.Set("prices", string(p)) 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. // 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("shipping_query_id", shippingQueryID)
vals.Set("ok", btoa(ok)) 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. // 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("pre_checkout_query_id", preCheckoutQueryID)
vals.Set("ok", btoa(ok)) 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. // GetStarTransactions returns the bot's Telegram Star transactions in chronological order.
func (a API) CreateInvoiceLink(title, description, payload, providerToken, currency string, prices []LabeledPrice, opts *CreateInvoiceLinkOptions) (res APIResponseBase, err error) { 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) var vals = make(url.Values)
p, err := json.Marshal(prices) vals.Set("user_id", itoa(userID))
if err != nil { vals.Set("telegram_payment_charge_id", telegramPaymentChargeID)
return res, err return res, a.client.get(a.base, "refundStarPayment", vals, &res)
}
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))
} }

View File

@@ -70,10 +70,6 @@ func scan(i any, v url.Values) url.Values {
return v return v
} }
func querify(i any) string {
return scan(i, url.Values{}).Encode()
}
func urlValues(i any) url.Values { func urlValues(i any) url.Values {
if i == nil { if i == nil {
return nil return nil

View File

@@ -20,7 +20,6 @@ package echotron
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/url" "net/url"
) )
@@ -50,8 +49,6 @@ type StickerSet struct {
Name string `json:"name"` Name string `json:"name"`
StickerType StickerSetType `json:"sticker_type"` StickerType StickerSetType `json:"sticker_type"`
Stickers []Sticker `json:"stickers"` 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 // 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. // InputSticker is a struct which describes a sticker to be added to a sticker set.
type InputSticker struct { type InputSticker struct {
Sticker InputFile `json:"-"`
EmojiList []string `json:"emoji_list"`
MaskPosition *MaskPosition `json:"mask_position,omitempty"` MaskPosition *MaskPosition `json:"mask_position,omitempty"`
Keywords *[]string `json:"keywords,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. // stickerEnvelope is a generic struct for all the various structs under the InputSticker interface.
type stickerEnvelope struct { type stickerEnvelope struct {
InputSticker
Sticker string `json:"sticker"` Sticker string `json:"sticker"`
InputSticker
} }
// SendSticker is used to send static .WEBP or animated .TGS stickers. // 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("sticker", stickerID)
vals.Set("chat_id", itoa(chatID)) 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. // 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) var vals = make(url.Values)
vals.Set("name", name) 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. // GetCustomEmojiStickers is used to get information about custom emoji stickers by their identifiers.
func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseStickers, err error) { func (a API) GetCustomEmojiStickers(customEmojiIDs ...string) (res APIResponseStickers, err error) {
var vals = make(url.Values)
jsn, _ := json.Marshal(customEmojiIDs) jsn, _ := json.Marshal(customEmojiIDs)
vals.Set("custom_emoji_ids", string(jsn))
var url = fmt.Sprintf( return res, a.client.get(a.base, "getCustomEmojiStickers", vals, &res)
"%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
} }
// UploadStickerFile is used to upload a .PNG file with a sticker for later use in // 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("user_id", itoa(userID))
vals.Set("sticker_format", string(format)) 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. // 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) var vals = make(url.Values)
vals.Set("user_id", itoa(userID)) vals.Set("user_id", itoa(userID))
vals.Set("name", name) vals.Set("name", name)
vals.Set("title", title) vals.Set("title", title)
vals.Set("sticker_format", string(format)) return res, a.client.postStickers(a.base, "createNewStickerSet", addValues(vals, opts), &res, stickers...)
return postStickers[APIResponseBool](a.base, "createNewStickerSet", addValues(vals, opts), stickers...)
} }
// AddStickerToSet is used to add a new sticker to a set created by the bot. // 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("user_id", itoa(userID))
vals.Set("name", name) 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. // 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("sticker", sticker)
vals.Set("position", itoa(int64(position))) 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. // 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) var vals = make(url.Values)
vals.Set("sticker", sticker) 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. // 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("sticker", sticker)
vals.Set("emoji_list", string(jsn)) 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. // 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("sticker", sticker)
vals.Set("keywords", string(jsn)) 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. // 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("sticker", sticker)
vals.Set("mask_position", string(jsn)) 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. // 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("name", name)
vals.Set("title", title) 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. // 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) var vals = make(url.Values)
vals.Set("name", name) vals.Set("name", name)
vals.Set("user_id", itoa(userID)) 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. // 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("name", name)
vals.Set("custom_emoji_id", emojiID) 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. // 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) var vals = make(url.Values)
vals.Set("name", name) 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. // 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) { 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)
} }

View File

@@ -23,28 +23,40 @@ import "encoding/json"
// Update represents an incoming update. // Update represents an incoming update.
// At most one of the optional parameters can be present in any given update. // At most one of the optional parameters can be present in any given update.
type Update struct { type Update struct {
ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"` ChatJoinRequest *ChatJoinRequest `json:"chat_join_request,omitempty"`
ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"` ChatBoost *ChatBoostUpdated `json:"chat_boost,omitempty"`
RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"` RemovedChatBoost *ChatBoostRemoved `json:"removed_chat_boost,omitempty"`
Message *Message `json:"message,omitempty"` Message *Message `json:"message,omitempty"`
EditedMessage *Message `json:"edited_message,omitempty"` EditedMessage *Message `json:"edited_message,omitempty"`
ChannelPost *Message `json:"channel_post,omitempty"` ChannelPost *Message `json:"channel_post,omitempty"`
EditedChannelPost *Message `json:"edited_channel_post,omitempty"` EditedChannelPost *Message `json:"edited_channel_post,omitempty"`
MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"` BusinessConnection *BusinessConnection `json:"business_connection,omitempty"`
MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"` BusinessMessage *Message `json:"business_message,omitempty"`
InlineQuery *InlineQuery `json:"inline_query,omitempty"` EditedBusinessMessage *Message `json:"edited_business_message,omitempty"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` DeletedBusinessMessages *BusinessMessagesDeleted `json:"deleted_business_messages,omitempty"`
CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` MessageReaction *MessageReactionUpdated `json:"message_reaction,omitempty"`
ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` MessageReactionCount *MessageReactionCountUpdated `json:"message_reaction_count,omitempty"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` InlineQuery *InlineQuery `json:"inline_query,omitempty"`
MyChatMember *ChatMemberUpdated `json:"my_chat_member,omitempty"` ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"`
ChatMember *ChatMemberUpdated `json:"chat_member,omitempty"` CallbackQuery *CallbackQuery `json:"callback_query,omitempty"`
ID int `json:"update_id"` 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"`
} }
// ChatID returns the ID of the chat the update is coming from. // ChatID returns the ID of the chat the update is coming from.
func (u Update) ChatID() int64 { func (u Update) ChatID() int64 {
switch { 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: case u.Message != nil:
return u.Message.Chat.ID return u.Message.Chat.ID
case u.EditedMessage != nil: case u.EditedMessage != nil:
@@ -53,6 +65,18 @@ func (u Update) ChatID() int64 {
return u.ChannelPost.Chat.ID return u.ChannelPost.Chat.ID
case u.EditedChannelPost != nil: case u.EditedChannelPost != nil:
return u.EditedChannelPost.Chat.ID 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: case u.InlineQuery != nil:
return u.InlineQuery.From.ID return u.InlineQuery.From.ID
case u.ChosenInlineResult != nil: case u.ChosenInlineResult != nil:
@@ -63,12 +87,12 @@ func (u Update) ChatID() int64 {
return u.ShippingQuery.From.ID return u.ShippingQuery.From.ID
case u.PreCheckoutQuery != nil: case u.PreCheckoutQuery != nil:
return u.PreCheckoutQuery.From.ID return u.PreCheckoutQuery.From.ID
case u.PollAnswer != nil:
return u.PollAnswer.User.ID
case u.MyChatMember != nil: case u.MyChatMember != nil:
return u.MyChatMember.Chat.ID return u.MyChatMember.Chat.ID
case u.ChatMember != nil: case u.ChatMember != nil:
return u.ChatMember.Chat.ID return u.ChatMember.Chat.ID
case u.ChatJoinRequest != nil:
return u.ChatJoinRequest.Chat.ID
default: default:
return 0 return 0
} }
@@ -215,9 +239,9 @@ func (a APIResponseString) Base() APIResponseBase {
} }
// APIResponseChat represents the incoming response from Telegram servers. // 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 { type APIResponseChat struct {
Result *Chat `json:"result,omitempty"` Result *ChatFullInfo `json:"result,omitempty"`
APIResponseBase APIResponseBase
} }
@@ -443,7 +467,7 @@ func (a APIResponseBotName) Base() APIResponseBase {
} }
// APIResponseUserChatBoosts represents the incoming response from Telegram servers. // 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 { type APIResponseUserChatBoosts struct {
Result *UserChatBoosts `json:"result,omitempty"` Result *UserChatBoosts `json:"result,omitempty"`
APIResponseBase APIResponseBase
@@ -454,6 +478,30 @@ func (a APIResponseUserChatBoosts) Base() APIResponseBase {
return a.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. // User represents a Telegram user or bot.
type User struct { type User struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
@@ -465,49 +513,67 @@ type User struct {
CanJoinGroups bool `json:"can_join_groups,omitempty"` CanJoinGroups bool `json:"can_join_groups,omitempty"`
CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"` CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"`
SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"`
CanConnectToBusiness bool `json:"can_connect_to_business,omitempty"`
IsPremium bool `json:"is_premium,omitempty"` IsPremium bool `json:"is_premium,omitempty"`
AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"` AddedToAttachmentMenu bool `json:"added_to_attachment_menu,omitempty"`
} }
// Chat represents a chat. // Chat represents a chat.
type Chat struct { type Chat struct {
Permissions *ChatPermissions `json:"permissions,omitempty"` Type string `json:"type"`
Location *ChatLocation `json:"location,omitempty"` Title string `json:"title,omitempty"`
PinnedMessage *Message `json:"pinned_message,omitempty"` Username string `json:"username,omitempty"`
Photo *ChatPhoto `json:"photo,omitempty"` FirstName string `json:"first_name,omitempty"`
ActiveUsernames *[]string `json:"active_usernames,omitempty"` LastName string `json:"last_name,omitempty"`
AvailableReactions *[]ReactionType `json:"available_reactions,omitempty"` ID int64 `json:"id"`
BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"` IsForum bool `json:"is_forum,omitempty"`
ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"` }
Bio string `json:"bio,omitempty"`
Username string `json:"username,omitempty"` // ChatFullInfo contains full information about a chat.
Title string `json:"title,omitempty"` type ChatFullInfo struct {
StickerSetName string `json:"sticker_set_name,omitempty"` Permissions *ChatPermissions `json:"permissions,omitempty"`
Description string `json:"description,omitempty"` Location *ChatLocation `json:"location,omitempty"`
FirstName string `json:"first_name,omitempty"` PinnedMessage *Message `json:"pinned_message,omitempty"`
LastName string `json:"last_name,omitempty"` Photo *ChatPhoto `json:"photo,omitempty"`
InviteLink string `json:"invite_link,omitempty"` ActiveUsernames *[]string `json:"active_usernames,omitempty"`
EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` AvailableReactions *[]ReactionType `json:"available_reactions,omitempty"`
Type string `json:"type"` BusinessIntro *BusinessIntro `json:"business_intro,omitempty"`
CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"` BusinessLocation *BusinessLocation `json:"business_location,omitempty"`
AccentColorID int `json:"accent_color_id,omitempty"` BusinessOpeningHours *BusinessOpeningHours `json:"business_opening_hours,omitempty"`
ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"` PersonalChat *Chat `json:"personal_chat,omitempty"`
EmojiStatusExpirationDate int `json:"emoji_status_expiration_date,omitempty"` Birthdate *Birthdate `json:"birthdate,omitempty"`
MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"` BackgroundCustomEmojiID string `json:"background_custom_emoji_id,omitempty"`
SlowModeDelay int `json:"slow_mode_delay,omitempty"` ProfileBackgroundCustomEmojiID string `json:"profile_background_custom_emoji_id,omitempty"`
UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"` Bio string `json:"bio,omitempty"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"` Username string `json:"username,omitempty"`
ID int64 `json:"id"` Title string `json:"title,omitempty"`
IsForum bool `json:"is_forum,omitempty"` StickerSetName string `json:"sticker_set_name,omitempty"`
HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"` Description string `json:"description,omitempty"`
HasHiddenMembers bool `json:"has_hidden_members,omitempty"` FirstName string `json:"first_name,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"` LastName string `json:"last_name,omitempty"`
HasVisibleHistory bool `json:"has_visible_history,omitempty"` InviteLink string `json:"invite_link,omitempty"`
HasPrivateForwards bool `json:"has_private_forwards,omitempty"` EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` Type string `json:"type"`
JoinToSendMessages bool `json:"join_to_send_messages,omitempty"` CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"`
JoinByRequest bool `json:"join_by_request,omitempty"` AccentColorID int `json:"accent_color_id,omitempty"`
HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,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"`
SlowModeDelay int `json:"slow_mode_delay,omitempty"`
UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"`
LinkedChatID int64 `json:"linked_chat_id,omitempty"`
ID int64 `json:"id"`
IsForum bool `json:"is_forum,omitempty"`
HasAggressiveAntiSpamEnabled bool `json:"has_aggressive_anti_spam_enabled,omitempty"`
HasHiddenMembers bool `json:"has_hidden_members,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"`
HasVisibleHistory bool `json:"has_visible_history,omitempty"`
HasPrivateForwards bool `json:"has_private_forwards,omitempty"`
CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"`
JoinToSendMessages bool `json:"join_to_send_messages,omitempty"`
JoinByRequest bool `json:"join_by_request,omitempty"`
HasRestrictedVoiceAndVideoMessages bool `json:"has_restricted_voice_and_video_messages,omitempty"`
} }
// Message represents a message. // Message represents a message.
@@ -561,12 +627,16 @@ type Message struct {
LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"`
ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"` ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"`
BoostAdded *ChatBoostAdded `json:"boost_added,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"` MediaGroupID string `json:"media_group_id,omitempty"`
ConnectedWebsite string `json:"connected_website,omitempty"` ConnectedWebsite string `json:"connected_website,omitempty"`
NewChatTitle string `json:"new_chat_title,omitempty"` NewChatTitle string `json:"new_chat_title,omitempty"`
AuthorSignature string `json:"author_signature,omitempty"` AuthorSignature string `json:"author_signature,omitempty"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
Text string `json:"text,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"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
NewChatPhoto []*PhotoSize `json:"new_chat_photo,omitempty"` NewChatPhoto []*PhotoSize `json:"new_chat_photo,omitempty"`
NewChatMembers []*User `json:"new_chat_members,omitempty"` NewChatMembers []*User `json:"new_chat_members,omitempty"`
@@ -588,6 +658,8 @@ type Message struct {
ChannelChatCreated bool `json:"channel_chat_created,omitempty"` ChannelChatCreated bool `json:"channel_chat_created,omitempty"`
HasProtectedContent bool `json:"has_protected_content,omitempty"` HasProtectedContent bool `json:"has_protected_content,omitempty"`
HasMediaSpoiler bool `json:"has_media_spoiler,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. // MessageID represents a unique message identifier.
@@ -701,8 +773,16 @@ type Dice struct {
// PollOption contains information about one answer option in a poll. // PollOption contains information about one answer option in a poll.
type PollOption struct { type PollOption struct {
Text string `json:"text"` Text string `json:"text"`
VoterCount int `json:"voter_count"` 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. // PollAnswer represents an answer of a user in a non-anonymous poll.
@@ -720,6 +800,7 @@ type Poll struct {
Explanation string `json:"explanation,omitempty"` Explanation string `json:"explanation,omitempty"`
ID string `json:"id"` ID string `json:"id"`
ExplanationEntities []*MessageEntity `json:"explanation_entities,omitempty"` ExplanationEntities []*MessageEntity `json:"explanation_entities,omitempty"`
QuestionEntities []*MessageEntity `json:"question_entities,omitempty"`
Options []*PollOption `json:"options"` Options []*PollOption `json:"options"`
OpenPeriod int `json:"open_period,omitempty"` OpenPeriod int `json:"open_period,omitempty"`
TotalVoterCount int `json:"total_voter_count"` TotalVoterCount int `json:"total_voter_count"`
@@ -885,12 +966,13 @@ type ChatMember struct {
// ChatMemberUpdated represents changes in the status of a chat member. // ChatMemberUpdated represents changes in the status of a chat member.
type ChatMemberUpdated struct { type ChatMemberUpdated struct {
InviteLink *ChatInviteLink `json:"invite_link,omitempty"` InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
Chat Chat `json:"chat"`
From User `json:"from"` From User `json:"from"`
OldChatMember ChatMember `json:"old_chat_member"` OldChatMember ChatMember `json:"old_chat_member"`
NewChatMember ChatMember `json:"new_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"` 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. // 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"` 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. // ChatLocation represents a location to which a chat is connected.
type ChatLocation struct { type ChatLocation struct {
Location *Location `json:"location"` Location *Location `json:"location"`
@@ -1025,12 +1139,13 @@ func (i mediaEnvelope) MarshalJSON() (cnt []byte, err error) {
// InputMediaPhoto represents a photo to be sent. // InputMediaPhoto represents a photo to be sent.
// Type MUST BE "photo". // Type MUST BE "photo".
type InputMediaPhoto struct { type InputMediaPhoto struct {
Type InputMediaType `json:"type"` Type InputMediaType `json:"type"`
Media InputFile `json:"-"` Media InputFile `json:"-"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"` ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
HasSpoiler bool `json:"has_spoiler,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. // media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
@@ -1045,17 +1160,18 @@ func (i InputMediaPhoto) groupable() {}
// InputMediaVideo represents a video to be sent. // InputMediaVideo represents a video to be sent.
// Type MUST BE "video". // Type MUST BE "video".
type InputMediaVideo struct { type InputMediaVideo struct {
Type InputMediaType `json:"type"` Type InputMediaType `json:"type"`
Media InputFile `json:"-"` Media InputFile `json:"-"`
Thumbnail InputFile `json:"-"` Thumbnail InputFile `json:"-"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"` ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"` Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"` Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"` Duration int `json:"duration,omitempty"`
SupportsStreaming bool `json:"supports_streaming,omitempty"` SupportsStreaming bool `json:"supports_streaming,omitempty"`
HasSpoiler bool `json:"has_spoiler,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. // media is a method which allows to obtain the Media (type InputFile) field from the InputMedia* struct.
@@ -1070,16 +1186,17 @@ func (i InputMediaVideo) groupable() {}
// InputMediaAnimation represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. // InputMediaAnimation represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
// Type MUST BE "animation". // Type MUST BE "animation".
type InputMediaAnimation struct { type InputMediaAnimation struct {
Type InputMediaType `json:"type"` Type InputMediaType `json:"type"`
Media InputFile `json:"-"` Media InputFile `json:"-"`
Thumbnail InputFile `json:"-"` Thumbnail InputFile `json:"-"`
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"` ParseMode ParseMode `json:"parse_mode,omitempty"`
CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"` CaptionEntities []*MessageEntity `json:"caption_entities,omitempty"`
Width int `json:"width,omitempty"` Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"` Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"` Duration int `json:"duration,omitempty"`
HasSpoiler bool `json:"has_spoiler,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. // 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 { type ChatJoinRequest struct {
InviteLink *ChatInviteLink `json:"invite_link,omitempty"` InviteLink *ChatInviteLink `json:"invite_link,omitempty"`
Bio string `json:"bio,omitempty"` Bio string `json:"bio,omitempty"`
From User `json:"user"`
Chat Chat `json:"chat"` Chat Chat `json:"chat"`
From User `json:"user"`
Date int `json:"date"` Date int `json:"date"`
UserChatID int64 `json:"user_chat_id"` UserChatID int64 `json:"user_chat_id"`
} }
@@ -1183,6 +1300,90 @@ type ChatBoostAdded struct {
BoostCount int `json:"boost_count"` 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. // ForumTopicCreated represents a service message about a new forum topic created in the chat.
type ForumTopicCreated struct { type ForumTopicCreated struct {
Name string `json:"name"` Name string `json:"name"`
@@ -1190,6 +1391,11 @@ type ForumTopicCreated struct {
IconColor int `json:"icon_color"` 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. // ForumTopicClosed represents a service message about a forum topic closed in the chat.
type ForumTopicClosed struct{} 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. // WriteAccessAllowed represents a service message about a user allowing a bot added to the attachment menu to write messages.
type WriteAccessAllowed struct { type WriteAccessAllowed struct {
FromRequest bool `json:"from_request,omitempty"`
WebAppName string `json:"web_app_name,omitempty"` WebAppName string `json:"web_app_name,omitempty"`
FromRequest bool `json:"from_request,omitempty"`
FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"` FromAttachmentMenu bool `json:"from_attachment_menu,omitempty"`
} }
@@ -1244,8 +1450,11 @@ type UserShared struct {
// ChatShared contains information about the chat whose identifier was shared with the bot using a KeyboardButtonRequestChat button. // ChatShared contains information about the chat whose identifier was shared with the bot using a KeyboardButtonRequestChat button.
type ChatShared struct { type ChatShared struct {
RequestID int `json:"request_id"` Photo *[]PhotoSize `json:"photo,omitempty"`
ChatID int64 `json:"chat_id"` Title string `json:"title,omitempty"`
Username string `json:"username,omitempty"`
RequestID int `json:"request_id"`
ChatID int64 `json:"chat_id"`
} }
// Story represents a story. // Story represents a story.
@@ -1254,28 +1463,12 @@ type Story struct {
ID int64 `json:"id"` ID int64 `json:"id"`
} }
type ReactionType interface { type ReactionType struct {
ImplementsReactionType()
}
// ReactionTypeEmoji is based on an emoji.
// Type is always "emoji".
type ReactionTypeEmoji 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"` Type string `json:"type"`
Emoji string `json:"emoji"`
CustomEmoji string `json:"custom_emoji"` 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. // ReactionCount represents a reaction added to a message along with the number of times it was added.
type ReactionCount struct { type ReactionCount struct {
Type ReactionType `json:"type"` 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. // MessageReactionUpdated represents a change of a reaction on a message performed by a user.
type MessageReactionUpdated struct { type MessageReactionUpdated struct {
Chat Chat `json:"chat"` Chat Chat `json:"chat"`
MessageID int `json:"message_id"`
User User `json:"user,omitempty"`
ActorChat Chat `json:"actor_chat,omitempty"` ActorChat Chat `json:"actor_chat,omitempty"`
Date int `json:"date"`
OldReaction []ReactionType `json:"old_reaction"` OldReaction []ReactionType `json:"old_reaction"`
NewReaction []ReactionType `json:"new_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. // MessageReactionCountUpdated represents reaction changes on a message with anonymous reactions.
type MessageReactionCountUpdated struct { type MessageReactionCountUpdated struct {
Reactions []ReactionCount `json:"reactions"`
Chat Chat `json:"chat"` Chat Chat `json:"chat"`
MessageID int `json:"message_id"` MessageID int `json:"message_id"`
Date int `json:"date"` 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. // TextQuote contains information about the quoted part of a message that is replied to by the given message.
type TextQuote struct { type TextQuote struct {
Entities *[]MessageEntity `json:"entities,omitempty"`
Text string `json:"text"`
Position int `json:"position"` Position int `json:"position"`
IsManual bool `json:"is_manual,omitempty"` 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. // ExternalReplyInfo contains information about a message that is being replied to, which may come from another chat or forum topic.
type ExternalReplyInfo struct { 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"` 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. // MessageOrigin describes the origin of a message.
type MessageOrigin struct { type MessageOrigin struct {
Date int `json:"date"` SenderChat *Chat `json:"sender_chat,omitempty"`
SenderUser *User `json:"sender_user,omitempty"`
Type string `json:"type"` Type string `json:"type"`
SenderUserName string `json:"sender_user_name,omitempty"` // Only for MessageOriginHiddenUser SenderUserName string `json:"sender_user_name,omitempty"`
AuthorSignature string `json:"author_signature,omitempty"` // Only for MessageOriginChannel AuthorSignature string `json:"author_signature,omitempty"`
SenderChat *Chat `json:"sender_chat,omitempty"` // Only for MessageOriginChat Date int `json:"date"`
SenderUser *User `json:"sender_user,omitempty"` // Only for MessageOriginUser
} }
// LinkPreviewOptions describes the options used for link preview generation. // LinkPreviewOptions describes the options used for link preview generation.
type LinkPreviewOptions struct { type LinkPreviewOptions struct {
IsDisabled bool `json:"is_disabled,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
IsDisabled bool `json:"is_disabled,omitempty"`
PreferSmallMedia bool `json:"prefer_small_media,omitempty"` PreferSmallMedia bool `json:"prefer_small_media,omitempty"`
PreferLargeMedia bool `json:"prefer_large_media,omitempty"` PreferLargeMedia bool `json:"prefer_large_media,omitempty"`
ShowAboveText bool `json:"show_above_text,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. // ReplyParameters describes reply parameters for the message that is being sent.
type ReplyParameters struct { 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"` Quote string `json:"quote,omitempty"`
QuoteParseMode string `json:"quote_parse_mode,omitempty"` QuoteParseMode string `json:"quote_parse_mode,omitempty"`
QuoteEntities []MessageEntity `json:"quote_entitites,omitempty"` QuoteEntities []MessageEntity `json:"quote_entitites,omitempty"`
MessageID int `json:"message_id"`
ChatID int64 `json:"chat_id,omitempty"`
QuotePosition int `json:"quote_position,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. // UsersShared contains information about the users whose identifiers were shared with the bot using a KeyboardButtonRequestUsers button.
type UsersShared struct { type UsersShared struct {
RequestID int `json:"request_id"` Users []SharedUser `json:"users"`
UserIDs []int64 `json:"user_ids"` RequestID int `json:"request_id"`
} }
// ChatBoost contains information about a chat boost. // ChatBoost contains information about a chat boost.
type ChatBoost struct { type ChatBoost struct {
BoostID string `json:"boost_id"` BoostID string `json:"boost_id"`
Source ChatBoostSource `json:"source"`
AddDate int `json:"add_date"` AddDate int `json:"add_date"`
ExpirationDate int `json:"expiration_date"` ExpirationDate int `json:"expiration_date"`
Source ChatBoostSource `json:"source"`
} }
// ChatBoostSourceType is a custom type for the various chat boost sources. // ChatBoostSourceType is a custom type for the various chat boost sources.
@@ -1392,8 +1594,8 @@ const (
// ChatBoostSource describes the source of a chat boost. // ChatBoostSource describes the source of a chat boost.
type ChatBoostSource struct { type ChatBoostSource struct {
Source ChatBoostSourceType `json:"source"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
Source ChatBoostSourceType `json:"source"`
GiveawayMessageID int `json:"giveaway_message_id,omitempty"` GiveawayMessageID int `json:"giveaway_message_id,omitempty"`
IsUnclaimed bool `json:"is_unclaimed,omitempty"` IsUnclaimed bool `json:"is_unclaimed,omitempty"`
} }
@@ -1406,10 +1608,10 @@ type ChatBoostUpdated struct {
// ChatBoostRemoved represents a boost removed from a chat. // ChatBoostRemoved represents a boost removed from a chat.
type ChatBoostRemoved struct { type ChatBoostRemoved struct {
Chat Chat `json:"chat"`
BoostID string `json:"boost_id"` BoostID string `json:"boost_id"`
RemoveDate int `json:"remove_date"` Chat Chat `json:"chat"`
Source ChatBoostSource `json:"source"` Source ChatBoostSource `json:"source"`
RemoveDate int `json:"remove_date"`
} }
// UserChatBoosts represents a list of boosts added to a chat by a user. // UserChatBoosts represents a list of boosts added to a chat by a user.
@@ -1417,39 +1619,57 @@ type UserChatBoosts struct {
Boosts []ChatBoost `json:"boosts"` 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. // Giveaway represents a message about a scheduled giveaway.
type Giveaway struct { type Giveaway struct {
CountryCodes *[]string `json:"country_codes,omitempty"`
PrizeDescription string `json:"prize_description,omitempty"`
Chats []Chat `json:"chats"` Chats []Chat `json:"chats"`
WinnersSelectionDate int `json:"winners_selection_date"` WinnersSelectionDate int `json:"winners_selection_date"`
WinnerCount int `json:"winner_count"` WinnerCount int `json:"winner_count"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
OnlyNewMembers bool `json:"only_new_members,omitempty"` OnlyNewMembers bool `json:"only_new_members,omitempty"`
HasPublicWinners bool `json:"has_public_winners,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{} type GiveawayCreated struct{}
// GiveawayWinners represents a message about the completion of a giveaway with public winners. // GiveawayWinners represents a message about the completion of a giveaway with public winners.
type GiveawayWinners struct { type GiveawayWinners struct {
PrizeDescription string `json:"prize_description,omitempty"`
Chats []Chat `json:"chats"` Chats []Chat `json:"chats"`
Winners []User `json:"winners"`
GiveawayMessageID int `json:"giveaway_message_id"` GiveawayMessageID int `json:"giveaway_message_id"`
WinnersSelectionDate int `json:"winners_selection_date"` WinnersSelectionDate int `json:"winners_selection_date"`
WinnerCount int `json:"winner_count"` WinnerCount int `json:"winner_count"`
Winners []User `json:"winners"`
AdditionalChatCount int `json:"additional_chat_count,omitempty"` AdditionalChatCount int `json:"additional_chat_count,omitempty"`
PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"` PremiumSubscriptionMonthCount int `json:"premium_subscription_month_count,omitempty"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"` UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
OnlyNewMembers bool `json:"only_new_members,omitempty"` OnlyNewMembers bool `json:"only_new_members,omitempty"`
WasRefunded bool `json:"was_refunded,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. // GiveawayCompleted represents a service message about the completion of a giveaway without public winners.
type GiveawayCompleted struct { type GiveawayCompleted struct {
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
WinnerCount int `json:"winner_count"` WinnerCount int `json:"winner_count"`
UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"` UnclaimedPrizeCount int `json:"unclaimed_prize_count,omitempty"`
GiveawayMessage *Message `json:"giveaway_message,omitempty"`
} }

View File

@@ -53,5 +53,5 @@ func (a API) AnswerWebAppQuery(webAppQueryID string, result InlineQueryResult) (
vals.Set("web_app_query_id", webAppQueryID) vals.Set("web_app_query_id", webAppQueryID)
vals.Set("result", string(resultJson)) 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
View 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
View 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

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014 Simon Eskildsen Copyright (c) 2015 Carles Cerezo Guzmán
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -9,13 +9,14 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in The above copyright notice and this permission notice shall be included in all
all copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
THE SOFTWARE. SOFTWARE.

58
vendor/github.com/carlescere/scheduler/README.md generated vendored Normal file
View File

@@ -0,0 +1,58 @@
# scheduler
[![GoDoc](https://godoc.org/github.com/carlescere/scheduler?status.svg)](https://godoc.org/github.com/carlescere/scheduler)
[![Build Status](https://travis-ci.org/carlescere/scheduler.svg?branch=master)](https://travis-ci.org/carlescere/scheduler)
[![Coverage Status](https://coveralls.io/repos/carlescere/scheduler/badge.svg?branch=master)](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
View 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
}

View File

@@ -70,3 +70,5 @@ benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics) - [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
- [FreeCache](https://github.com/coocood/freecache) - [FreeCache](https://github.com/coocood/freecache)
- [FastCache](https://github.com/VictoriaMetrics/fastcache) - [FastCache](https://github.com/VictoriaMetrics/fastcache)
- [Ristretto](https://github.com/dgraph-io/ristretto)
- [Badger](https://github.com/dgraph-io/badger)

View File

@@ -19,10 +19,13 @@ const (
// Store the primes in an array as well. // Store the primes in an array as well.
// //
// The consts are used when possible in Go code to avoid MOVs but we need a // 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} var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
// Digest implements hash.Hash64. // 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 { type Digest struct {
v1 uint64 v1 uint64
v2 uint64 v2 uint64
@@ -33,19 +36,31 @@ type Digest struct {
n int // how much of mem is used 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 { func New() *Digest {
return NewWithSeed(0)
}
// NewWithSeed creates a new Digest with the given seed.
func NewWithSeed(seed uint64) *Digest {
var d Digest var d Digest
d.Reset() d.ResetWithSeed(seed)
return &d return &d
} }
// Reset clears the Digest's state so that it can be reused. // Reset clears the Digest's state so that it can be reused.
// It uses a seed value of zero.
func (d *Digest) Reset() { func (d *Digest) Reset() {
d.v1 = primes[0] + prime2 d.ResetWithSeed(0)
d.v2 = prime2 }
d.v3 = 0
d.v4 = -primes[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.total = 0
d.n = 0 d.n = 0
} }

View File

@@ -6,7 +6,7 @@
package xxhash 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 //go:noescape
func Sum64(b []byte) uint64 func Sum64(b []byte) uint64

View File

@@ -3,7 +3,7 @@
package xxhash 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 { func Sum64(b []byte) uint64 {
// A simpler version would be // A simpler version would be
// d := New() // d := New()

View File

@@ -5,7 +5,7 @@
package xxhash 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 { func Sum64String(s string) uint64 {
return Sum64([]byte(s)) return Sum64([]byte(s))
} }

View File

@@ -33,7 +33,7 @@ import (
// //
// See https://github.com/golang/go/issues/42739 for discussion. // 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. // It may be faster than Sum64([]byte(s)) by avoiding a copy.
func Sum64String(s string) uint64 { func Sum64String(s string) uint64 {
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})) b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))

View File

@@ -25,7 +25,7 @@ import (
// WithContextTags captures the k/v pairs stored in the context via the // WithContextTags captures the k/v pairs stored in the context via the
// `logtags` package and annotates them on the error. // `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 // because the library cannot guarantee that the underlying value is
// preserved across the network. To avoid creating a stateful interface // preserved across the network. To avoid creating a stateful interface
// (where the user code needs to know whether an error has traveled // (where the user code needs to know whether an error has traveled

View File

@@ -14,6 +14,6 @@
// Package errutil combines primitives from the library, the Go errors // Package errutil combines primitives from the library, the Go errors
// package and github.com/pkg/errors. It aims to serve as drop-in // 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`. // implementation of `pgerrors`.
package errutil package errutil

View File

@@ -179,6 +179,13 @@ func IsAny(err error, references ...error) bool {
return true 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. // Try harder with marks.

25
vendor/github.com/cockroachdb/fifo/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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) },
}

View File

@@ -2142,7 +2142,6 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) {
// calculation even when the WAL is disabled. // calculation even when the WAL is disabled.
metrics.BytesIn = metrics.BytesFlushed metrics.BytesIn = metrics.BytesFlushed
} else { } else {
metrics := c.metrics[0]
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
metrics.BytesIn += d.mu.mem.queue[i].logSize metrics.BytesIn += d.mu.mem.queue[i].logSize
} }

View File

@@ -377,7 +377,7 @@ type DB struct {
queue []fileInfo queue []fileInfo
// The number of input bytes to the log. This is the raw size of the // 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 // 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 bytesIn uint64
// The LogWriter is protected by commitPipeline.mu. This allows log // The LogWriter is protected by commitPipeline.mu. This allows log
// writes to be performed without holding DB.mu, but requires both // 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 var prevLogSize uint64
if !d.opts.DisableWAL { if !d.opts.DisableWAL {
now := time.Now() now := time.Now()
newLogNum, prevLogSize = d.recycleWAL() newLogNum, prevLogSize = d.rotateWAL()
if b != nil { if b != nil {
b.commitStats.WALRotationDuration += time.Since(now) b.commitStats.WALRotationDuration += time.Since(now)
} }
@@ -2543,15 +2543,29 @@ func (d *DB) rotateMemtable(newLogNum FileNum, logSeqNum uint64, prev *memTable)
var entry *flushableEntry var entry *flushableEntry
d.mu.mem.mutable, entry = d.newMemTable(newLogNum, logSeqNum) d.mu.mem.mutable, entry = d.newMemTable(newLogNum, logSeqNum)
d.mu.mem.queue = append(d.mu.mem.queue, entry) 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) d.updateReadStateLocked(nil)
if prev.writerUnref() { if prev.writerUnref() {
d.maybeScheduleFlush() 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 // Both DB.mu and commitPipeline.mu must be held by the caller. Note that DB.mu
// may be released and reacquired. // 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 { if d.opts.DisableWAL {
panic("pebble: invalid function call") panic("pebble: invalid function call")
} }

View File

@@ -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 // We create a new WAL for the flushable instead of reusing the end of
// the previous WAL. This simplifies the increment of the minimum // the previous WAL. This simplifies the increment of the minimum
// unflushed log number, and also simplifies WAL replay. // 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() d.mu.Unlock()
err := d.commit.directWrite(b) err := d.commit.directWrite(b)
if err != nil { if err != nil {
@@ -1243,13 +1248,14 @@ func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error
// the appropriate value below. // the appropriate value below.
newLogNum := d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum newLogNum := d.mu.mem.queue[len(d.mu.mem.queue)-1].logNum
if !d.opts.DisableWAL { if !d.opts.DisableWAL {
// This is WAL num of the next mutable memtable which comes after the // newLogNum will be the WAL num of the next mutable memtable which
// ingestedFlushable in the flushable queue. The mutable memtable // comes after the ingestedFlushable in the flushable queue. The mutable
// will be created below. // memtable will be created below.
newLogNum, _ = d.recycleWAL() //
if err != nil { // The prevLogSize returned by rotateWAL is the WAL to which the
return err // 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 currMem := d.mu.mem.mutable

View File

@@ -7,6 +7,9 @@ package base
import ( import (
"fmt" "fmt"
"time" "time"
"github.com/cockroachdb/pebble/internal/humanize"
"github.com/cockroachdb/redact"
) )
// InternalIterator iterates over a DB's key/value pairs in key order. Unlike // 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.ValueBytes += from.SeparatedPointValue.ValueBytes
s.SeparatedPointValue.ValueBytesFetched += from.SeparatedPointValue.ValueBytesFetched 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))
}
}

View File

@@ -154,6 +154,18 @@ func (s *RangeKeyIteratorStats) Merge(o RangeKeyIteratorStats) {
s.SkippedPoints += o.SkippedPoints 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. // LazyValue is a lazy value. See the long comment in base.LazyValue.
type LazyValue = base.LazyValue type LazyValue = base.LazyValue
@@ -2788,44 +2800,44 @@ func (stats *IteratorStats) String() string {
// SafeFormat implements the redact.SafeFormatter interface. // SafeFormat implements the redact.SafeFormatter interface.
func (stats *IteratorStats) SafeFormat(s redact.SafePrinter, verb rune) { func (stats *IteratorStats) SafeFormat(s redact.SafePrinter, verb rune) {
for i := range stats.ForwardStepCount { if stats.ReverseSeekCount[InterfaceCall] == 0 && stats.ReverseSeekCount[InternalIterCall] == 0 {
switch IteratorStatsKind(i) { s.Printf("seeked %s times (%s internal)",
case InterfaceCall: humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall])),
s.SafeString("(interface (dir, seek, step): ") humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InternalIterCall])),
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.InternalStats.SeparatedPointValue.Count != 0 { } else {
s.Printf(", (separated: (count %s, bytes %s, fetched %s)))", s.Printf("seeked %s times (%s fwd/%s rev, internal: %s fwd/%s rev)",
humanize.Count.Uint64(stats.InternalStats.SeparatedPointValue.Count), humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall]+stats.ReverseSeekCount[InterfaceCall])),
humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytes), humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InterfaceCall])),
humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytesFetched)) humanize.Count.Uint64(uint64(stats.ReverseSeekCount[InterfaceCall])),
} else { humanize.Count.Uint64(uint64(stats.ForwardSeekCount[InternalIterCall])),
s.Printf(")") 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{}) { if stats.RangeKeyStats != (RangeKeyIteratorStats{}) {
s.SafeString(",\n(range-key-stats: ") s.SafeString(", ")
s.Printf("(count %d), (contained points: (count %d, skipped %d)))", stats.RangeKeyStats.SafeFormat(s, verb)
stats.RangeKeyStats.Count,
stats.RangeKeyStats.ContainedPoints,
stats.RangeKeyStats.SkippedPoints)
} }
} }

View File

@@ -92,6 +92,17 @@ type Settings struct {
// out a large chunk of dirty filesystem buffers. // out a large chunk of dirty filesystem buffers.
BytesPerSync int 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 // Fields here are set only if the provider is to support remote objects
// (experimental). // (experimental).
Remote struct { 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), // DefaultSettings initializes default settings (with no remote storage),
// suitable for tests and tools. // suitable for tests and tools.
func DefaultSettings(fs vfs.FS, dirName string) Settings { func DefaultSettings(fs vfs.FS, dirName string) Settings {

View File

@@ -4,6 +4,8 @@
package objstorageprovider package objstorageprovider
import "github.com/cockroachdb/pebble/internal/invariants"
const ( const (
// Constants for dynamic readahead of data blocks. Note that the size values // 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 // 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 // Returns a size value (greater than 0) that should be prefetched if readahead
// would be beneficial. // would be beneficial.
func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
if invariants.Enabled && rs.maxReadaheadSize == 0 {
panic("readaheadState not initialized")
}
currentReadEnd := offset + blockLength currentReadEnd := offset + blockLength
if rs.numReads >= minFileReadsForReadahead { if rs.numReads >= minFileReadsForReadahead {
// The minimum threshold of sequential reads to justify reading ahead // The minimum threshold of sequential reads to justify reading ahead

View File

@@ -31,7 +31,11 @@ func (p *provider) vfsOpenForReading(
} }
return nil, err 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( func (p *provider) vfsCreate(

View File

@@ -27,22 +27,26 @@ type fileReadable struct {
// The following fields are used to possibly open the file again using the // The following fields are used to possibly open the file again using the
// sequential reads option (see vfsReadHandle). // sequential reads option (see vfsReadHandle).
filename string filename string
fs vfs.FS fs vfs.FS
readaheadConfig ReadaheadConfig
} }
var _ objstorage.Readable = (*fileReadable)(nil) 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() info, err := file.Stat()
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := &fileReadable{ r := &fileReadable{
file: file, file: file,
size: info.Size(), size: info.Size(),
filename: filename, filename: filename,
fs: fs, fs: fs,
readaheadConfig: readaheadConfig,
} }
invariants.SetFinalizer(r, func(obj interface{}) { invariants.SetFinalizer(r, func(obj interface{}) {
if obj.(*fileReadable).file != nil { if obj.(*fileReadable).file != nil {
@@ -76,14 +80,14 @@ func (r *fileReadable) Size() int64 {
// NewReadHandle is part of the objstorage.Readable interface. // NewReadHandle is part of the objstorage.Readable interface.
func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
rh := readHandlePool.Get().(*vfsReadHandle) rh := readHandlePool.Get().(*vfsReadHandle)
rh.r = r rh.init(r)
rh.rs = makeReadaheadState(fileMaxReadaheadSize)
return rh return rh
} }
type vfsReadHandle struct { type vfsReadHandle struct {
r *fileReadable r *fileReadable
rs readaheadState rs readaheadState
readaheadMode ReadaheadMode
// sequentialFile holds a file descriptor to the same underlying File, // sequentialFile holds a file descriptor to the same underlying File,
// except with fadvise(FADV_SEQUENTIAL) called on it to take advantage of // 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. // Close is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) Close() error { func (rh *vfsReadHandle) Close() error {
var err error var err error
@@ -121,14 +133,17 @@ func (rh *vfsReadHandle) Close() error {
// ReadAt is part of the objstorage.ReadHandle interface. // ReadAt is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error { func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error {
var n int
var err error
if rh.sequentialFile != nil { if rh.sequentialFile != nil {
// Use OS-level read-ahead. // Use OS-level read-ahead.
n, err = rh.sequentialFile.ReadAt(p, offset) n, err := rh.sequentialFile.ReadAt(p, offset)
} else { 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 := 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 // We've reached the maximum readahead size. Beyond this point, rely on
// OS-level readahead. // OS-level readahead.
rh.switchToOSReadahead() rh.switchToOSReadahead()
@@ -136,8 +151,8 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
_ = rh.r.file.Prefetch(offset, readaheadSize) _ = 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) { if invariants.Enabled && err == nil && n != len(p) {
panic("short read") 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. // SetupForCompaction is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) SetupForCompaction() { func (rh *vfsReadHandle) SetupForCompaction() {
rh.switchToOSReadahead() rh.readaheadMode = rh.r.readaheadConfig.Informed
if rh.readaheadMode == FadviseSequential {
rh.switchToOSReadahead()
}
} }
func (rh *vfsReadHandle) switchToOSReadahead() { func (rh *vfsReadHandle) switchToOSReadahead() {
if invariants.Enabled && rh.readaheadMode != FadviseSequential {
panic("readheadMode not respected")
}
if rh.sequentialFile != nil { if rh.sequentialFile != nil {
return return
} }
@@ -164,8 +185,8 @@ func (rh *vfsReadHandle) switchToOSReadahead() {
// RecordCacheHit is part of the objstorage.ReadHandle interface. // RecordCacheHit is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) { func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {
if rh.sequentialFile != nil { if rh.sequentialFile != nil || rh.readaheadMode == NoReadahead {
// Using OS-level readahead, so do nothing. // Using OS-level or no readahead, so do nothing.
return return
} }
rh.rs.recordCacheHit(offset, size) rh.rs.recordCacheHit(offset, size)
@@ -208,8 +229,7 @@ func UsePreallocatedReadHandle(
ctx context.Context, readable objstorage.Readable, rh *PreallocatedReadHandle, ctx context.Context, readable objstorage.Readable, rh *PreallocatedReadHandle,
) objstorage.ReadHandle { ) objstorage.ReadHandle {
if r, ok := readable.(*fileReadable); ok { if r, ok := readable.(*fileReadable); ok {
// See fileReadable.NewReadHandle. rh.init(r)
rh.vfsReadHandle = vfsReadHandle{r: r}
return rh return rh
} }
return readable.NewReadHandle(ctx) return readable.NewReadHandle(ctx)

View File

@@ -310,6 +310,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) {
NoSyncOnClose: opts.NoSyncOnClose, NoSyncOnClose: opts.NoSyncOnClose,
BytesPerSync: opts.BytesPerSync, BytesPerSync: opts.BytesPerSync,
} }
providerSettings.Local.ReadaheadConfigFn = opts.Local.ReadaheadConfigFn
providerSettings.Remote.StorageFactory = opts.Experimental.RemoteStorage providerSettings.Remote.StorageFactory = opts.Experimental.RemoteStorage
providerSettings.Remote.CreateOnShared = opts.Experimental.CreateOnShared providerSettings.Remote.CreateOnShared = opts.Experimental.CreateOnShared
providerSettings.Remote.CreateOnSharedLocator = opts.Experimental.CreateOnSharedLocator providerSettings.Remote.CreateOnSharedLocator = opts.Experimental.CreateOnSharedLocator

View File

@@ -14,11 +14,13 @@ import (
"time" "time"
"github.com/cockroachdb/errors" "github.com/cockroachdb/errors"
"github.com/cockroachdb/fifo"
"github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/cache"
"github.com/cockroachdb/pebble/internal/humanize" "github.com/cockroachdb/pebble/internal/humanize"
"github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/keyspan"
"github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/manifest"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
"github.com/cockroachdb/pebble/objstorage/remote" "github.com/cockroachdb/pebble/objstorage/remote"
"github.com/cockroachdb/pebble/rangekey" "github.com/cockroachdb/pebble/rangekey"
"github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/pebble/sstable"
@@ -481,11 +483,25 @@ type Options struct {
// The default cache size is 8 MB. // The default cache size is 8 MB.
Cache *cache.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
// Cleaner cleans obsolete files. // Cleaner cleans obsolete files.
// //
// The default cleaner uses the DeleteCleaner. // The default cleaner uses the DeleteCleaner.
Cleaner Cleaner 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 // Comparer defines a total ordering over the space of []byte keys: a 'less
// than' relationship. The same comparison algorithm must be used for reads // than' relationship. The same comparison algorithm must be used for reads
// and writes over the lifetime of the DB. // 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. // DebugCheckLevels calls CheckLevels on the provided database.
// It may be set in the DebugCheck field of Options to check // It may be set in the DebugCheck field of Options to check
// level invariants whenever a new version is installed. // level invariants whenever a new version is installed.
@@ -1713,6 +1732,7 @@ func (o *Options) MakeReaderOptions() sstable.ReaderOptions {
var readerOpts sstable.ReaderOptions var readerOpts sstable.ReaderOptions
if o != nil { if o != nil {
readerOpts.Cache = o.Cache readerOpts.Cache = o.Cache
readerOpts.LoadBlockSema = o.LoadBlockSema
readerOpts.Comparer = o.Comparer readerOpts.Comparer = o.Comparer
readerOpts.Filters = o.Filters readerOpts.Filters = o.Filters
if o.Merger != nil { if o.Merger != nil {

View File

@@ -5,6 +5,7 @@
package sstable package sstable
import ( import (
"github.com/cockroachdb/fifo"
"github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/cache"
) )
@@ -111,6 +112,11 @@ type ReaderOptions struct {
// The default cache size is a zero-size cache. // The default cache size is a zero-size cache.
Cache *cache.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. // User properties specified in this map will not be added to sst.Properties.UserProperties.
DeniedUserProperties map[string]struct{} DeniedUserProperties map[string]struct{}

View File

@@ -539,6 +539,15 @@ func (r *Reader) readBlock(
} }
// Cache miss. // 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 var compressed cacheValueOrBuf
if bufferPool != nil { if bufferPool != nil {
compressed = cacheValueOrBuf{ compressed = cacheValueOrBuf{
@@ -571,6 +580,7 @@ func (r *Reader) readBlock(
int(bh.Length+blockTrailerLen), readDuration.String()) int(bh.Length+blockTrailerLen), readDuration.String())
} }
if stats != nil { if stats != nil {
stats.BlockBytes += bh.Length
stats.BlockReadDuration += readDuration stats.BlockReadDuration += readDuration
} }
if err != nil { if err != nil {
@@ -628,9 +638,6 @@ func (r *Reader) readBlock(
decompressed = transformed decompressed = transformed
} }
if stats != nil {
stats.BlockBytes += bh.Length
}
if decompressed.buf.Valid() { if decompressed.buf.Valid() {
return bufferHandle{b: decompressed.buf}, nil return bufferHandle{b: decompressed.buf}, nil
} }

View File

@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider"
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing"
) )
@@ -177,7 +178,7 @@ func (i *twoLevelIterator) init(
_ = i.topLevelIndex.Close() _ = i.topLevelIndex.Close()
return err return err
} }
i.dataRH = r.readable.NewReadHandle(ctx) i.dataRH = objstorageprovider.UsePreallocatedReadHandle(ctx, r.readable, &i.dataRHPrealloc)
if r.tableFormat >= TableFormatPebblev3 { if r.tableFormat >= TableFormatPebblev3 {
if r.Properties.NumValueBlocks > 0 { if r.Properties.NumValueBlocks > 0 {
i.vbReader = &valueBlockReader{ i.vbReader = &valueBlockReader{

View File

@@ -5,6 +5,7 @@
package vfs package vfs
import ( import (
"fmt"
"io" "io"
"os" "os"
) )
@@ -38,7 +39,15 @@ func (fs *loggingFS) Create(name string) (File, error) {
} }
func (fs *loggingFS) Open(name string, opts ...OpenOption) (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...) f, err := fs.FS.Open(name, opts...)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -1,5 +1,34 @@
# Changelog # 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 ## 0.27.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0. The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0.

View File

@@ -13,7 +13,6 @@
[![Build Status](https://github.com/getsentry/sentry-go/workflows/go-workflow/badge.svg)](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow) [![Build Status](https://github.com/getsentry/sentry-go/workflows/go-workflow/badge.svg)](https://github.com/getsentry/sentry-go/actions?query=workflow%3Ago-workflow)
[![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go) [![Go Report Card](https://goreportcard.com/badge/github.com/getsentry/sentry-go)](https://goreportcard.com/report/github.com/getsentry/sentry-go)
[![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr)
[![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
[![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go) [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
`sentry-go` provides a Sentry client implementation for the Go programming `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) - [Bug Tracker](https://github.com/getsentry/sentry-go/issues)
- [GitHub Project](https://github.com/getsentry/sentry-go) - [GitHub Project](https://github.com/getsentry/sentry-go)
- [![GoDoc](https://godoc.org/github.com/getsentry/sentry-go?status.svg)](https://godoc.org/github.com/getsentry/sentry-go)
- [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go) - [![go.dev](https://img.shields.io/badge/go.dev-pkg-007d9c.svg?style=flat)](https://pkg.go.dev/github.com/getsentry/sentry-go)
- [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/) - [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/platforms/go/)
- [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions) - [![Discussions](https://img.shields.io/github/discussions/getsentry/sentry-go.svg)](https://github.com/getsentry/sentry-go/discussions)

View File

@@ -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. // 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 // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
Timezone string `json:"timezone,omitempty"` 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 type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout

View File

@@ -90,11 +90,10 @@ func NewDsn(rawURL string) (*Dsn, error) {
// Port // Port
var port int var port int
if parsedURL.Port() != "" { if parsedURL.Port() != "" {
parsedPort, err := strconv.Atoi(parsedURL.Port()) port, err = strconv.Atoi(parsedURL.Port())
if err != nil { if err != nil {
return nil, &DsnParseError{"invalid port"} return nil, &DsnParseError{"invalid port"}
} }
port = parsedPort
} else { } else {
port = scheme.defaultPort() port = scheme.defaultPort()
} }

View File

@@ -140,7 +140,7 @@ func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event
for _, suspect := range suspects { for _, suspect := range suspects {
for _, pattern := range iei.ignoreErrors { 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."+ Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern) "| Value matched: %s | Filter used: %s", suspect, pattern)
return nil return nil
@@ -202,7 +202,7 @@ func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint)
} }
for _, pattern := range iei.ignoreTransactions { 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."+ Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern) "| Value matched: %s | Filter used: %s", suspect, pattern)
return nil return nil

View File

@@ -3,6 +3,7 @@ package sentry
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@@ -24,6 +25,9 @@ const profileType = "profile"
// checkInType is the type of a check in event. // checkInType is the type of a check in event.
const checkInType = "check_in" const checkInType = "check_in"
// metricType is the type of a metric event.
const metricType = "statsd"
// Level marks the severity of the event. // Level marks the severity of the event.
type Level string type Level string
@@ -36,15 +40,6 @@ const (
LevelFatal Level = "fatal" 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. // SdkInfo contains all metadata about about the SDK being used.
type SdkInfo struct { type SdkInfo struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@@ -170,6 +165,13 @@ type Request struct {
Env map[string]string `json:"env,omitempty"` 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 returns a new Sentry Request from the given http.Request.
// //
// NewRequest avoids operations that depend on network access. In particular, it // 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} env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
} }
} else { } else {
sensitiveHeaders := getSensitiveHeaders()
for k, v := range r.Header { for k, v := range r.Header {
if _, ok := sensitiveHeaders[k]; !ok { if _, ok := sensitiveHeaders[k]; !ok {
headers[k] = strings.Join(v, ",") headers[k] = strings.Join(v, ",")
@@ -222,11 +223,15 @@ func NewRequest(r *http.Request) *Request {
// Mechanism is the mechanism by which an exception was generated and handled. // Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct { type Mechanism struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
HelpLink string `json:"help_link,omitempty"` HelpLink string `json:"help_link,omitempty"`
Handled *bool `json:"handled,omitempty"` Source string `json:"source,omitempty"`
Data map[string]interface{} `json:"data,omitempty"` Handled *bool `json:"handled,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. // SetUnhandled indicates that the exception is an unhandled exception, i.e.
@@ -318,6 +323,7 @@ type Event struct {
Exception []Exception `json:"exception,omitempty"` Exception []Exception `json:"exception,omitempty"`
DebugMeta *DebugMeta `json:"debug_meta,omitempty"` DebugMeta *DebugMeta `json:"debug_meta,omitempty"`
Attachments []*Attachment `json:"-"` Attachments []*Attachment `json:"-"`
Metrics []Metric `json:"-"`
// The fields below are only relevant for transactions. // 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. // SetException appends the unwrapped errors to the event's exception list.
// //
// maxErrorDepth is the maximum depth of the error chain we will look // 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) { func (e *Event) SetException(exception error, maxErrorDepth int) {
err := exception if exception == nil {
if err == nil {
return 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{ e.Exception = append(e.Exception, Exception{
Value: err.Error(), Value: err.Error(),
Type: reflect.TypeOf(err).String(), Type: reflect.TypeOf(err).String(),
Stacktrace: ExtractStacktrace(err), Stacktrace: ExtractStacktrace(err),
}) })
switch previous := err.(type) {
case interface{ Unwrap() error }: // Attempt to unwrap the error using the standard library's Unwrap method.
err = previous.Unwrap() // If errors.Unwrap returns nil, it means either there is no error to unwrap,
case interface{ Cause() error }: // or the error does not implement the Unwrap method.
err = previous.Cause() unwrappedErr := errors.Unwrap(err)
default:
err = nil 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 // 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() e.Exception[0].Stacktrace = NewStacktrace()
} }
if len(e.Exception) <= 1 {
return
}
// event.Exception should be sorted such that the most recent error is last. // event.Exception should be sorted such that the most recent error is last.
reverse(e.Exception) 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, // TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
@@ -492,13 +529,12 @@ func (e *Event) checkInMarshalJSON() ([]byte, error) {
// NewEvent creates a new Event. // NewEvent creates a new Event.
func NewEvent() *Event { func NewEvent() *Event {
event := Event{ return &Event{
Contexts: make(map[string]Context), Contexts: make(map[string]Context),
Extra: make(map[string]interface{}), Extra: make(map[string]interface{}),
Tags: make(map[string]string), Tags: make(map[string]string),
Modules: make(map[string]string), Modules: make(map[string]string),
} }
return &event
} }
// Thread specifies threads that were running at the time of an event. // Thread specifies threads that were running at the time of an event.

View File

@@ -32,15 +32,14 @@ var knownCategories = map[Category]struct{}{
// String returns the category formatted for debugging. // String returns the category formatted for debugging.
func (c Category) String() string { func (c Category) String() string {
switch c { if c == "" {
case "":
return "CategoryAll" return "CategoryAll"
default:
caser := cases.Title(language.English)
rv := "Category"
for _, w := range strings.Fields(string(c)) {
rv += caser.String(w)
}
return rv
} }
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
View 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]
})
}

View File

@@ -6,7 +6,7 @@ import (
) )
// The version of the SDK. // 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 // apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK. // sentry-go SDK.
@@ -72,18 +72,17 @@ func Recover() *EventID {
// RecoverWithContext captures a panic and passes relevant context object. // RecoverWithContext captures a panic and passes relevant context object.
func RecoverWithContext(ctx context.Context) *EventID { func RecoverWithContext(ctx context.Context) *EventID {
if err := recover(); err != nil { err := recover()
var hub *Hub if err == nil {
return nil
if HasHubOnContext(ctx) {
hub = GetHubFromContext(ctx)
} else {
hub = CurrentHub()
}
return hub.RecoverWithContext(ctx, err)
} }
return nil
hub := GetHubFromContext(ctx)
if hub == nil {
hub = CurrentHub()
}
return hub.RecoverWithContext(ctx, err)
} }
// WithScope is a shorthand for CurrentHub().WithScope. // WithScope is a shorthand for CurrentHub().WithScope.

View File

@@ -7,8 +7,8 @@ import (
// Checks whether the transaction should be profiled (according to ProfilesSampleRate) // Checks whether the transaction should be profiled (according to ProfilesSampleRate)
// and starts a profiler if so. // and starts a profiler if so.
func (span *Span) sampleTransactionProfile() { func (s *Span) sampleTransactionProfile() {
var sampleRate = span.clientOptions().ProfilesSampleRate var sampleRate = s.clientOptions().ProfilesSampleRate
switch { switch {
case sampleRate < 0.0 || sampleRate > 1.0: case sampleRate < 0.0 || sampleRate > 1.0:
Logger.Printf("Skipping transaction profiling: ProfilesSampleRate out of range [0.0, 1.0]: %f\n", sampleRate) 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 { if globalProfiler == nil {
Logger.Println("Skipping transaction profiling: the profiler couldn't be started") Logger.Println("Skipping transaction profiling: the profiler couldn't be started")
} else { } else {
span.collectProfile = collectTransactionProfile s.collectProfile = collectTransactionProfile
} }
} }
} }
@@ -69,22 +69,27 @@ func (info *profileInfo) UpdateFromEvent(event *Event) {
info.Dist = event.Dist info.Dist = event.Dist
info.Transaction.ID = event.EventID 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 runtimeContext, ok := event.Contexts["runtime"]; ok {
if value, ok := runtimeContext["name"]; !ok { info.Runtime.Name = getStringFromContext(runtimeContext, info.Runtime.Name, "name")
info.Runtime.Name = value.(string) info.Runtime.Version = getStringFromContext(runtimeContext, info.Runtime.Version, "version")
}
if value, ok := runtimeContext["version"]; !ok {
info.Runtime.Version = value.(string)
}
} }
if osContext, ok := event.Contexts["os"]; ok { if osContext, ok := event.Contexts["os"]; ok {
if value, ok := osContext["name"]; !ok { info.OS.Name = getStringFromContext(osContext, info.OS.Name, "name")
info.OS.Name = value.(string)
}
} }
if deviceContext, ok := event.Contexts["device"]; ok { if deviceContext, ok := event.Contexts["device"]; ok {
if value, ok := deviceContext["arch"]; !ok { info.Device.Architecture = getStringFromContext(deviceContext, info.Device.Architecture, "arch")
info.Device.Architecture = value.(string)
}
} }
} }

View File

@@ -169,11 +169,11 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
span.Sampled = span.sample() span.Sampled = span.sample()
span.recorder = &spanRecorder{}
if hasParent { if hasParent {
span.recorder = parent.spanRecorder() span.recorder = parent.spanRecorder()
} else {
span.recorder = &spanRecorder{}
} }
span.recorder.record(&span) span.recorder.record(&span)
hub := hubFromContext(ctx) 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 // 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 // accessing the data map directly as SetData takes care of initializing the map
// when necessary. // when necessary.
func (s *Span) SetData(name, value string) { func (s *Span) SetData(name string, value interface{}) {
if value == nil {
return
}
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()

View File

@@ -2,6 +2,7 @@ package sentry
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -94,6 +95,55 @@ func getRequestBodyFromEvent(event *Event) []byte {
return nil 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 { func encodeAttachment(enc *json.Encoder, b io.Writer, attachment *Attachment) error {
// Attachment header // Attachment header
err := enc.Encode(struct { err := enc.Encode(struct {
@@ -175,11 +225,15 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
return nil, err return nil, err
} }
if event.Type == transactionType || event.Type == checkInType { switch event.Type {
case transactionType, checkInType:
err = encodeEnvelopeItem(enc, event.Type, body) err = encodeEnvelopeItem(enc, event.Type, body)
} else { case metricType:
err = encodeMetric(enc, &b, event.Metrics)
default:
err = encodeEnvelopeItem(enc, eventType, body) err = encodeEnvelopeItem(enc, eventType, body)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -206,7 +260,7 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
return &b, nil 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() { defer func() {
if r != nil { if r != nil {
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", event.Sdk.Name, event.Sdk.Version)) 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 { if err != nil {
return nil, err return nil, err
} }
return http.NewRequest(
if ctx == nil {
ctx = context.Background()
}
return http.NewRequestWithContext(
ctx,
http.MethodPost, http.MethodPost,
dsn.GetAPIURL().String(), dsn.GetAPIURL().String(),
envelope, 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) { 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 { if t.dsn == nil {
return return
} }
@@ -356,7 +421,7 @@ func (t *HTTPTransport) SendEvent(event *Event) {
return return
} }
request, err := getRequestFromEvent(event, t.dsn) request, err := getRequestFromEvent(ctx, event, t.dsn)
if err != nil { if err != nil {
return return
} }
@@ -478,6 +543,13 @@ func (t *HTTPTransport) worker() {
Logger.Printf("There was an issue with sending an event: %v", err) Logger.Printf("There was an issue with sending an event: %v", err)
continue 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.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response)) t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock() 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) { 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 { if t.dsn == nil {
return return
} }
@@ -577,15 +654,18 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
return return
} }
request, err := getRequestFromEvent(event, t.dsn) request, err := getRequestFromEvent(ctx, event, t.dsn)
if err != nil { if err != nil {
return return
} }
var eventType string var eventType string
if event.Type == transactionType { switch {
case event.Type == transactionType:
eventType = "transaction" eventType = "transaction"
} else { case event.Type == metricType:
eventType = metricType
default:
eventType = fmt.Sprintf("%s event", event.Level) eventType = fmt.Sprintf("%s event", event.Level)
} }
Logger.Printf( Logger.Printf(
@@ -601,6 +681,14 @@ func (t *HTTPSyncTransport) SendEvent(event *Event) {
Logger.Printf("There was an issue with sending an event: %v", err) Logger.Printf("There was an issue with sending an event: %v", err)
return 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.mu.Lock()
t.limits.Merge(ratelimit.FromResponse(response)) t.limits.Merge(ratelimit.FromResponse(response))
t.mu.Unlock() t.mu.Unlock()

View File

@@ -112,3 +112,7 @@ func revisionFromBuildInfo(info *debug.BuildInfo) string {
return "" return ""
} }
func Pointer[T any](v T) *T {
return &v
}

View File

@@ -55,6 +55,10 @@ https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/comp
* s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839 * s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
* flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837 * 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 * 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) * 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 * zstd: Fix default level first dictionary encode https://github.com/klauspost/compress/pull/829
@@ -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: Add LZ4 block converter. https://github.com/klauspost/compress/pull/748
* s2: Support io.ReaderAt in ReadSeeker. https://github.com/klauspost/compress/pull/747 * 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 * s2c/s2sx: Use concurrent decoding. https://github.com/klauspost/compress/pull/746
</details>
<details> <details>
<summary>See changes to v1.15.x</summary> <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). 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 # Stateless compression
This package offers stateless compression as a special option for gzip/deflate. This package offers stateless compression as a special option for gzip/deflate.

View File

@@ -51,7 +51,7 @@ func emitCopy(dst []byte, offset, length int) int {
i := 0 i := 0
// The maximum length for a single tagCopy1 or tagCopy2 op is 64 bytes. The // 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 // 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 // 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 // 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 // a length 64 tagCopy2 followed by a length 3 tagCopy2 (which encodes as

View File

@@ -554,6 +554,9 @@ func (b *blockDec) prepareSequences(in []byte, hist *history) (err error) {
if debugDecoder { if debugDecoder {
printf("Compression modes: 0b%b", compMode) 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++ { for i := uint(0); i < 3; i++ {
mode := seqCompMode((compMode >> (6 - i*2)) & 3) mode := seqCompMode((compMode >> (6 - i*2)) & 3)
if debugDecoder { if debugDecoder {

Some files were not shown because too many files have changed in this diff Show More