0

Compare commits

..

19 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
32 changed files with 2018 additions and 403 deletions

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:

67
TODO.md
View File

@@ -1,67 +0,0 @@
# Перечень того, что надо реализовать.
В качестве основы возьмём https://github.com/NicoNex/echotron
Для реализации телеграммного фронтэнда нам надо сделать:
-1. Перелелать layout на "стандартный" гошный.
0. Парсер конфига.
[V] Реализован начальный вариант парсера конфига.
1. Разгребальщик апдейтов и формирователь redis-ных сообщений.
[V] Написана заглушка для парсера апдейтов bot api телеграма
[V] Написан парсер прочих команд
2. Разгребальщик редисных сообщений и формирователь запросов в tg bot api.
[V] Написан черновой вариант разгребателя
[ ] Написан мигратор для группового чата. (апгрейд до супергруппы, до чата с тредами)
3. Обработчик группы команд admin, сохранение настроек в pebbledb.
1 [V] [V] цензор сообщений определённого типа.
2 [ ] [V] фортунка каждое утро. (+ добавить время, относительно gmt, когда показывать фортунку).
3 [V] [V] Приветствие новых участников
4 [V] [V] Прощание с теми, кто ушёл
5 [ ] [ ] oboobs
6 [ ] [ ] obutts
7 [V] [V] chan_msg (Пока что бот сам удяляет сообщения от таких имперсонализированных пользователей, однако в прроде
есть возможность таких персонажей банить, см banChatSenderChat()/unbanChatSenderChat()
https://core.telegram.org/bots/api#banchatsenderchat, надо ли реализовывать такой бан - над этим надо ещё
подумать.)
8 [ ] [ ] user ban
9 [ ] [ ] user mute
4. [V] help
5. [V] help admin
6. [ ] Readme
7. [ ] Поддержать миграцию групп в супергруппы (итд, там меняется id и надо перекидывать настройки на новый id)
8. [ ] Поддержать парсинг ошибок отправки сообщений в телегу (в т.ч. мигратор для группового чата, также внятные сообщения в лог, если бот замьючен)
9. [ ] Поддержать leaveChat(), чтобы бот мог легально и самостоятельно покинуть чят.
10. [ ] ставить реакцию thumbsup на спасибо, пожалуйста итд, ставить сердечко, если похвалили - молодц, красава итд. (setMessageReaction() на соответствующий ответ на сообщение)
11. [ ] отслеживать участников чятика, чтобы была возможность мьютить, банить, узнавать инфу по разным участникам чятика.
12. [ ] промоут/демоут участника до админа
frontend -> router -> service -> frontend

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" : {
"token" : "Long_string_given_by_@BotFather"
},
# Prefix-sign for all commands. In many irc-bots (eggdrop/windrop) it is "." (dot), in sulci it was "!". # 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. # 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" : "!", "csign" : "!",
"token" : "Long_string_given_by_@BotFather"
},
# 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:

3
go.mod
View File

@@ -1,9 +1,10 @@
module aleesa-telegram-go module aleesa-telegram-go
go 1.22 go 1.25
require ( require (
github.com/NicoNex/echotron/v3 v3.33.1 github.com/NicoNex/echotron/v3 v3.33.1
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
github.com/cockroachdb/errors v1.11.3 github.com/cockroachdb/errors v1.11.3
github.com/cockroachdb/pebble v1.1.1 github.com/cockroachdb/pebble v1.1.1
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1

2
go.sum
View File

@@ -4,6 +4,8 @@ github.com/NicoNex/echotron/v3 v3.33.1 h1:P9bVvmnuVd0C7bR5q9TV6Pece/wBqZ6PSo0Rqo
github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q= github.com/NicoNex/echotron/v3 v3.33.1/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI=
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=

View File

@@ -1,3 +1,4 @@
// Package log wraps log/slog for more easier and convinient use.
package log package log
import ( import (
@@ -11,17 +12,19 @@ import (
"time" "time"
) )
var (
// Handler exposes slog handler struct. // Handler exposes slog handler struct.
var Handler slog.Handler Handler slog.Handler
// Ctx log context. For now it is just sits here and do nothing. // Ctx log context. For now it is just sits here and do nothing.
var Ctx = context.Background() Ctx = context.Background()
// Writer go-writer eported for capturing other types of logs, even for std log go facility. // Writer go-writer eported for capturing other types of logs, even for std log go facility.
var Writer io.Writer Writer io.Writer
// reader is required for Writer to work properly. It is connected via io.Pipe() to Writer. // reader is required for Writer to work properly. It is connected via io.Pipe() to Writer.
var reader io.Reader reader io.Reader
)
// Init setup logger stuff. // Init setup logger stuff.
// level can be error, warn, info, debug if something other supplied info level selected. // level can be error, warn, info, debug if something other supplied info level selected.
@@ -29,7 +32,6 @@ var reader io.Reader
func Init(level string, fileDescriptor *os.File) { func Init(level string, fileDescriptor *os.File) {
var loglevel slog.Level var loglevel slog.Level
// no panic, no trace.
switch level { switch level {
case "debug": case "debug":
loglevel = slog.LevelDebug loglevel = slog.LevelDebug
@@ -48,18 +50,17 @@ func Init(level string, fileDescriptor *os.File) {
} }
opts := &slog.HandlerOptions{ //nolint: exhaustruct opts := &slog.HandlerOptions{ //nolint: exhaustruct
// Use the ReplaceAttr function on the handler options // Use the ReplaceAttr function on the handler options to be able to replace any single attribute in the log
// to be able to replace any single attribute in the log output // output.
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive
// check that we are handling the time key // Check that we are handling the time key.
if a.Key != slog.TimeKey { if a.Key != slog.TimeKey {
return a return a
} }
t := a.Value.Time() t := a.Value.Time()
// change the value from a time.Time to a String // Change the value from a time.Time to the String. The string has correct time format.
// where the string has the correct time format.
a.Value = slog.StringValue(t.Format(time.DateTime)) a.Value = slog.StringValue(t.Format(time.DateTime))
return a return a
@@ -85,7 +86,7 @@ func readIoPipe() {
for { for {
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() // Get the current line as a string line := scanner.Text() // Get the current line as a string.
slog.Debug(line) slog.Debug(line)
} }

View File

@@ -1,8 +1,10 @@
// Package tg implements most of aleesa-telegram-go functionality.
package tg package tg
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strconv"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
@@ -18,113 +20,110 @@ func Admin(msg *echotron.Update) (bool, error) {
switch { switch {
case c == "admin" || c == "админ": case c == "admin" || c == "админ":
answer = "```\n" answer = "```\n"
answer += fmt.Sprintf("%sadmin censor - показать список состояния типов сообщений\n", Config.Csign)
answer += fmt.Sprintf("%sадмин ценз - показать список состояния типов сообщений\n", Config.Csign) answer += Config.Csign + "admin censor - показать список состояния типов сообщений\n"
answer += fmt.Sprintf("%sadmin censor type # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n", Config.Csign) answer += Config.Csign + "админ ценз - показать список состояния типов сообщений\n"
answer += fmt.Sprintf("%sадмин ценз тип # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n", Config.Csign) answer += Config.Csign + "admin censor type # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n"
answer += fmt.Sprintf("%sadmin fortune - показываем ли с утра фортунку для чата\n", Config.Csign) answer += Config.Csign + "админ ценз тип # - где 1 - вкл, 0 - выкл цензуры для означенного типа сообщений\n"
answer += fmt.Sprintf("%sadmin фортунка - показываем ли с утра фортунку для чата\n", Config.Csign) answer += Config.Csign + "admin fortune - показываем ли с утра фортунку для чата\n"
answer += fmt.Sprintf("%sadmin fortune # - где 1 - вкл, 0 - выкл фортунку с утра\n", Config.Csign) answer += Config.Csign + "admin фортунка - показываем ли с утра фортунку для чата\n"
answer += fmt.Sprintf("%sadmin фортунка # - где 1 - вкл, 0 - выкл фортунку с утра\n", Config.Csign) answer += Config.Csign + "admin fortune # - где 1 - вкл, 0 - выкл фортунку с утра\n"
answer += fmt.Sprintf("%sadmin greet - приветствуем ли новых участников чата\n", Config.Csign) answer += Config.Csign + "admin фортунка # - где 1 - вкл, 0 - выкл фортунку с утра\n"
answer += fmt.Sprintf("%sadmin приветствие - приветствуем ли новых участников чата\n", Config.Csign) answer += Config.Csign + "admin greet - приветствуем ли новых участников чата\n"
answer += fmt.Sprintf("%sadmin greet # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n", Config.Csign) answer += Config.Csign + "admin приветствие - приветствуем ли новых участников чата\n"
answer += fmt.Sprintf("%sadmin приветствие # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n", Config.Csign) answer += Config.Csign + "admin greet # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n"
answer += fmt.Sprintf("%sadmin goodbye - прощаемся ли с ушедшими участниками чата\n", Config.Csign) answer += Config.Csign + "admin приветствие # - где 1 - вкл, 0 - выкл приветствия новых участников чата\n"
answer += fmt.Sprintf("%sadmin прощание - прощаемся ли с ушедшими участниками чата\n", Config.Csign) answer += Config.Csign + "admin goodbye - прощаемся ли с ушедшими участниками чата\n"
answer += fmt.Sprintf("%sadmin goodbye # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n", Config.Csign) answer += Config.Csign + "admin прощание - прощаемся ли с ушедшими участниками чата\n"
answer += fmt.Sprintf("%sadmin прощание # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n", Config.Csign) answer += Config.Csign + "admin goodbye # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n"
answer += fmt.Sprintf("%sadmin oboobs # - где 1 - вкл, 0 - выкл плагина oboobs\n", Config.Csign) answer += Config.Csign + "admin прощание # - где 1 - вкл, 0 - выкл прощания с ушедшими участниками чата\n"
answer += Config.Csign + "admin oboobs # - где 1 - вкл, 0 - выкл плагина oboobs\n"
answer += fmt.Sprintf( answer += fmt.Sprintf(
"%sadmin oboobs - показываем ли сисечки по просьбе участников чата (команды %stits, %stities, %sboobs, %sboobies, %sсиси, %sсисечки)\n", "%sadmin oboobs - показываем ли сисечки по просьбе участников чата (команды %stits, %stities, %sboobs, %sboobies, %sсиси, %sсисечки)\n",
Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign,
) )
answer += fmt.Sprintf("%sadmin obutts # - где 1 - вкл, 0 - выкл плагина obutts\n", Config.Csign)
answer += Config.Csign + "admin obutts # - где 1 - вкл, 0 - выкл плагина obutts\n"
answer += fmt.Sprintf( answer += fmt.Sprintf(
"%sadmin obutts - показываем ли попки по просьбе участников чата (команды %sass, %sbutt, %sbooty, %sпопа, %sпопка)\n", "%sadmin obutts - показываем ли попки по просьбе участников чата (команды %sass, %sbutt, %sbooty, %sпопа, %sпопка)\n",
Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign, Config.Csign,
) )
answer += fmt.Sprintf("%sadmin chan_msg - оставляем ли сообщения присланные от имени (других) каналов\n", Config.Csign)
answer += fmt.Sprintf("%sadmin chan_msg # - где 1 - оставляем, 0 - удаляем\n", Config.Csign) answer += Config.Csign + "admin chan_msg - оставляем ли сообщения присланные от имени (других) каналов\n"
answer += fmt.Sprintf( answer += Config.Csign + "admin chan_msg # - где 1 - оставляем, 0 - удаляем\n"
"%sadmin ban userid sec - выдаём ban указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n", answer += Config.Csign + "admin ban userid sec - выдаём ban указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n"
Config.Csign, answer += Config.Csign + "admin mute userid sec - выдаём mute указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n"
) answer += Config.Csign + "admin admin mute - разрешено ли обычным админам мьютить участников чата через бота (если бот - админ), (создатель чата всегда может попросить бота-админа замьютить обычного участника чата)\n"
answer += fmt.Sprintf(
"%sadmin mute userid sec - выдаём mute указанному user-у на указанное количество секунд (от 30 сек до 1 года), доступно только создателю чата\n", answer += Config.Csign + "%sadmin admin mute # - где 1 - разрешено, 0 - не разрешено\n"
Config.Csign,
)
answer += fmt.Sprintf(
"%sadmin admin mute - разрешено ли обычным админам мьютить участников чата через бота (если бот - админ), (создатель чата всегда может попросить бота-админа замьютить обычного участника чата)\n",
Config.Csign,
)
answer += fmt.Sprintf("%sadmin admin mute # - где 1 - разрешено, 0 - не разрешено\n", Config.Csign)
answer += "\n" answer += "\n"
answer += "Типы сообщений:\naudio voice photo video video_note animation sticker dice game poll document\n" answer += "Типы сообщений:\naudio voice photo video video_note animation sticker dice game poll document\n"
answer += "```" answer += "```"
case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)$").MatchString(c): case regexp.MustCompile("^(admin|админ)[[:space:]](censor|ценз)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg") == "1" {
answer += "Тип сообщения voice удаляется\n" answer += "Тип сообщения voice удаляется\n"
} else { } else {
answer += "Тип сообщения voice не удаляется\n" answer += "Тип сообщения voice не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg") == "1" {
answer += "Тип сообщения audio удаляется\n" answer += "Тип сообщения audio удаляется\n"
} else { } else {
answer += "Тип сообщения audio не удаляется\n" answer += "Тип сообщения audio не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg") == "1" {
answer += "Тип сообщения photo удаляется\n" answer += "Тип сообщения photo удаляется\n"
} else { } else {
answer += "Тип сообщения photo не удаляется\n" answer += "Тип сообщения photo не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg") == "1" {
answer += "Тип сообщения video удаляется\n" answer += "Тип сообщения video удаляется\n"
} else { } else {
answer += "Тип сообщения video не удаляется\n" answer += "Тип сообщения video не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg") == "1" {
answer += "Тип сообщения video_note удаляется\n" answer += "Тип сообщения video_note удаляется\n"
} else { } else {
answer += "Тип сообщения video_note не удаляется\n" answer += "Тип сообщения video_note не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg") == "1" {
answer += "Тип сообщения animation удаляется\n" answer += "Тип сообщения animation удаляется\n"
} else { } else {
answer += "Тип сообщения animation не удаляется\n" answer += "Тип сообщения animation не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg") == "1" {
answer += "Тип сообщения sticker удаляется\n" answer += "Тип сообщения sticker удаляется\n"
} else { } else {
answer += "Тип сообщения sticker не удаляется\n" answer += "Тип сообщения sticker не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DiceMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DiceMsg") == "1" {
answer += "Тип сообщения dice удаляется\n" answer += "Тип сообщения dice удаляется\n"
} else { } else {
answer += "Тип сообщения dice не удаляется\n" answer += "Тип сообщения dice не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg") == "1" {
answer += "Тип сообщения game удаляется\n" answer += "Тип сообщения game удаляется\n"
} else { } else {
answer += "Тип сообщения game не удаляется\n" answer += "Тип сообщения game не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg") == "1" {
answer += "Тип сообщения poll удаляется\n" answer += "Тип сообщения poll удаляется\n"
} else { } else {
answer += "Тип сообщения poll не удаляется\n" answer += "Тип сообщения poll не удаляется\n"
} }
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg") == "1" {
answer += "Тип сообщения document удаляется\n" answer += "Тип сообщения document удаляется\n"
} else { } else {
answer += "Тип сообщения document не удаляется\n" answer += "Тип сообщения document не удаляется\n"
@@ -139,14 +138,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "voice": case "voice":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с voice будут оставаться" answer += "Теперь сообщения с voice будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VoiceMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VoiceMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -159,14 +158,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "audio": case "audio":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с audio будут оставаться" answer += "Теперь сообщения с audio будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AudioMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AudioMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -179,14 +178,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "photo": case "photo":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с photo будут оставаться" answer += "Теперь сообщения с photo будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PhotoMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PhotoMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -199,14 +198,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "video": case "video":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с video будут оставаться" answer += "Теперь сообщения с video будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -219,14 +218,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "video_note": case "video_note":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с video_note будут оставаться" answer += "Теперь сообщения с video_note будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "VideoNoteMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "VideoNoteMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -239,14 +238,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "animation": case "animation":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с animation будут оставаться" answer += "Теперь сообщения с animation будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "AnimationMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "AnimationMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -259,14 +258,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "sticker": case "sticker":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с sticker будут оставаться" answer += "Теперь сообщения с sticker будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "StickerMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "StickerMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -279,14 +278,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "dice": case "dice":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DiceMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DiceMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с dice будут оставаться" answer += "Теперь сообщения с dice будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "diceMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "diceMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -299,14 +298,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "game": case "game":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с game будут оставаться" answer += "Теперь сообщения с game будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GameMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GameMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -319,14 +318,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "poll": case "poll":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с poll будут оставаться" answer += "Теперь сообщения с poll будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "PollMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "PollMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -339,14 +338,14 @@ func Admin(msg *echotron.Update) (bool, error) {
case "document": case "document":
switch cmdArray[3] { switch cmdArray[3] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с document будут оставаться" answer += "Теперь сообщения с document будут оставаться"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "DocumentMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "DocumentMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -361,7 +360,7 @@ func Admin(msg *echotron.Update) (bool, error) {
} }
case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)$").MatchString(c): case regexp.MustCompile("^(admin|админ)[[:space:]](fortune|фортунка)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg") == "1" {
answer += "Фортунки с утра показываются" answer += "Фортунки с утра показываются"
} else { } else {
answer += "Фортунки с утра не показываются" answer += "Фортунки с утра не показываются"
@@ -375,14 +374,14 @@ func Admin(msg *echotron.Update) (bool, error) {
// мы не сможем вытащить список чятиков. // мы не сможем вытащить список чятиков.
switch cmdArray[2] { switch cmdArray[2] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Теперь сообщения с фортункой будут отправляться с утра" answer += "Теперь сообщения с фортункой будут отправляться с утра"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "FortuneMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "FortuneMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -393,7 +392,7 @@ func Admin(msg *echotron.Update) (bool, error) {
} }
case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)$").MatchString(c): case regexp.MustCompile("^(admin|админ)[[:space:]](greet|приветствие)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg") == "1" {
answer += "Приветствуем вновьприбывших участников чатика" answer += "Приветствуем вновьприбывших участников чатика"
} else { } else {
answer += "Не приветствуем вновьприбывших участников чатика" answer += "Не приветствуем вновьприбывших участников чатика"
@@ -407,14 +406,14 @@ func Admin(msg *echotron.Update) (bool, error) {
// мы не сможем вытащить список чятиков. // мы не сможем вытащить список чятиков.
switch cmdArray[2] { switch cmdArray[2] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Будем приветствовать вновьприбывших участников чата" answer += "Будем приветствовать вновьприбывших участников чата"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -425,7 +424,7 @@ func Admin(msg *echotron.Update) (bool, error) {
} }
case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)$").MatchString(c): case regexp.MustCompile("^(admin|админ)[[:space:]](goodbye|прощание)$").MatchString(c):
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg") == "1" {
answer += "Прощаемся с ушедшими участниками чатика" answer += "Прощаемся с ушедшими участниками чатика"
} else { } else {
answer += "Не прощаемся с ушедшими участниками чатика" answer += "Не прощаемся с ушедшими участниками чатика"
@@ -437,14 +436,14 @@ func Admin(msg *echotron.Update) (bool, error) {
switch cmdArray[2] { switch cmdArray[2] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Будем прощаться с ушедшими участниками чата" answer += "Будем прощаться с ушедшими участниками чата"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg", "0"); err != nil {
return true, err return true, err
} }
@@ -460,14 +459,14 @@ func Admin(msg *echotron.Update) (bool, error) {
switch cmdArray[2] { switch cmdArray[2] {
case "1": case "1":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "ChanMsg", "1"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "ChanMsg", "1"); err != nil {
return true, err return true, err
} }
answer += "Будем удалять сообщения, написанные от имени канала" answer += "Будем удалять сообщения, написанные от имени канала"
case "0": case "0":
if err := SaveSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "ChanMsg", "0"); err != nil { if err := SaveSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "ChanMsg", "0"); err != nil {
return true, err return true, err
} }

View File

@@ -1,7 +1,7 @@
package tg package tg
import ( import (
"fmt" "strconv"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
) )
@@ -10,10 +10,13 @@ import (
// Непотребными могут быть аудиосообщения, аудиофайлы, видеосообщения, сообщения от имени других каналов итп. // Непотребными могут быть аудиосообщения, аудиофайлы, видеосообщения, сообщения от имени других каналов итп.
// Это могут настроить админы чятика через команду !admin censor. // Это могут настроить админы чятика через команду !admin censor.
func Censor(msg *echotron.Update) bool { func Censor(msg *echotron.Update) bool {
result := false var (
result bool
chatID = msg.ChatID()
)
switch { switch {
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VoiceMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "VoiceMsg") == "1":
result = true result = true
// Предполагаем, что у voice-ов здесь всегда не ноль. // Предполагаем, что у voice-ов здесь всегда не ноль.
@@ -23,7 +26,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "AudioMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "AudioMsg") == "1":
// Предполагаем, что у аудио здесь всегда не ноль. // Предполагаем, что у аудио здесь всегда не ноль.
if msg.Message.Audio.Duration != 0 { if msg.Message.Audio.Duration != 0 {
delMsg(msg) delMsg(msg)
@@ -31,7 +34,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "PhotoMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "PhotoMsg") == "1":
// Обычное сообщение не содержит фоток. // Обычное сообщение не содержит фоток.
if len(msg.Message.Photo) != 0 { if len(msg.Message.Photo) != 0 {
delMsg(msg) delMsg(msg)
@@ -39,7 +42,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VideoMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "VideoMsg") == "1":
// Предполагаем, что у видео здесь всегда не ноль. // Предполагаем, что у видео здесь всегда не ноль.
if msg.Message.Video.Duration != 0 { if msg.Message.Video.Duration != 0 {
delMsg(msg) delMsg(msg)
@@ -47,7 +50,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "VideoNoteMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "VideoNoteMsg") == "1":
// Предполагаем, что у видео-заметки здесь всегда не ноль. // Предполагаем, что у видео-заметки здесь всегда не ноль.
if msg.Message.VideoNote.Duration != 0 { if msg.Message.VideoNote.Duration != 0 {
delMsg(msg) delMsg(msg)
@@ -55,7 +58,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "AnimationMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "AnimationMsg") == "1":
// Предполагаем, что у анимации здесь всегда не ноль. // Предполагаем, что у анимации здесь всегда не ноль.
if msg.Message.Animation.Duration != 0 { if msg.Message.Animation.Duration != 0 {
delMsg(msg) delMsg(msg)
@@ -63,7 +66,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "StickerMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "StickerMsg") == "1":
// Предполагаем, что FileID не пустое только у стикера. // Предполагаем, что FileID не пустое только у стикера.
if msg.Message.Sticker.FileID != "" { if msg.Message.Sticker.FileID != "" {
delMsg(msg) delMsg(msg)
@@ -71,7 +74,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "DiceMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "DiceMsg") == "1":
// Предполагаем, что Value > 0 только у дайса. // Предполагаем, что Value > 0 только у дайса.
if msg.Message.Dice.Value != 0 { if msg.Message.Dice.Value != 0 {
delMsg(msg) delMsg(msg)
@@ -79,7 +82,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "GameMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "GameMsg") == "1":
// Предполагаем, что title только у game-а. // Предполагаем, что title только у game-а.
if msg.Message.Game.Title != "" { if msg.Message.Game.Title != "" {
delMsg(msg) delMsg(msg)
@@ -87,7 +90,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "PollMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "PollMsg") == "1":
// Предполагаем, что title только у game-а. // Предполагаем, что title только у game-а.
if msg.Message.Poll.Question != "" { if msg.Message.Poll.Question != "" {
delMsg(msg) delMsg(msg)
@@ -95,7 +98,7 @@ func Censor(msg *echotron.Update) bool {
result = true result = true
} }
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "DocumentMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "DocumentMsg") == "1":
// Предполагаем, что FileID есть только у document-а. // Предполагаем, что FileID есть только у document-а.
if msg.Message.Document.FileID != "" { if msg.Message.Document.FileID != "" {
delMsg(msg) delMsg(msg)
@@ -107,7 +110,7 @@ func Censor(msg *echotron.Update) bool {
// пидорство он будет удалять asap. // пидорство он будет удалять asap.
// 136817688 - это специальный id пользователя, который принимает облик канала, на него можно нажать и попасть // 136817688 - это специальный id пользователя, который принимает облик канала, на него можно нажать и попасть
// на рекламируемый канал. // на рекламируемый канал.
case GetSetting(fmt.Sprintf("%d", msg.ChatID()), "ChanMsg") == "1": case GetSetting(strconv.FormatInt(chatID, 10), "ChanMsg") == "1":
if msg.Message.From.ID == 136817688 { if msg.Message.From.ID == 136817688 {
delMsg(msg) delMsg(msg)

View File

@@ -5,6 +5,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp" "regexp"
"slices"
"strconv"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
) )
@@ -20,8 +22,8 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
smsg.From = "telegram" smsg.From = "telegram"
smsg.Plugin = "telegram" smsg.Plugin = "telegram"
smsg.Chatid = fmt.Sprintf("%d", cmd.Message.Chat.ID) smsg.Chatid = strconv.FormatInt(cmd.Message.Chat.ID, 10)
smsg.Userid = fmt.Sprintf("%d", cmd.Message.From.ID) smsg.Userid = strconv.FormatInt(cmd.Message.From.ID, 10)
smsg.Message = cmd.Message.Text smsg.Message = cmd.Message.Text
smsg.Misc.Answer = 1 smsg.Misc.Answer = 1
@@ -60,12 +62,8 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
"рыба", "рыбка", "рыбалка", "karma", "карма", "fuck", "weather", "погода", "w", "п", "погодка", "погадка", "рыба", "рыбка", "рыбалка", "karma", "карма", "fuck", "weather", "погода", "w", "п", "погодка", "погадка",
} }
for _, c := range cmds { if slices.Contains(cmds, command) {
if command == c {
bingo = true bingo = true
break
}
} }
// Не нашлось команды в одно слово. Поищем команды с одним аргументом. // Не нашлось команды в одно слово. Поищем команды с одним аргументом.
@@ -103,20 +101,14 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка", "dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка",
} }
for _, c := range cmds { if slices.Contains(cmds, command) {
// В vscode возникает ошибка при использовании if-а.
switch {
case command == c:
smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From) smsg.Misc.Username = ConstructTelegramHighlightName(cmd.Message.From)
smsg.Misc.Msgformat = 1 smsg.Misc.Msgformat = 1
break //nolint: gosimple
}
} }
// Если у нас супергруппа с тредами, надо проставить thread id. // Если у нас супергруппа с тредами, надо проставить thread id.
if cmd.Message.Chat.Type == "supergroup" && cmd.Message.IsTopicMessage && cmd.Message.ThreadID != 0 { if cmd.Message.Chat.Type == "supergroup" && cmd.Message.IsTopicMessage && cmd.Message.ThreadID != 0 {
smsg.ThreadID = fmt.Sprintf("%d", cmd.Message.ThreadID) smsg.ThreadID = strconv.Itoa(cmd.Message.ThreadID)
} }
// Отправляем сообщение в редиску. // Отправляем сообщение в редиску.
@@ -129,7 +121,7 @@ func cmdParser(me echotron.APIResponseUser, cmd *echotron.Update) (bool, error)
} }
// Заталкиваем наш json в редиску. // Заталкиваем наш json в редиску.
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil { 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) log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else { } else {
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data)) log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))

View File

@@ -5,36 +5,53 @@ import (
"os" "os"
"github.com/NicoNex/echotron/v3" "github.com/NicoNex/echotron/v3"
"github.com/carlescere/scheduler"
"github.com/cockroachdb/pebble" "github.com/cockroachdb/pebble"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
) )
var (
// Config - это у нас глобальная штука. // Config - это у нас глобальная штука.
var Config myConfig Config MyConfig
// To break circular message forwarding we must set some sane default, it can be overridden via config. // forwardMax is used to break circular message forwarding we must set some sane default, it can be overridden via config.
var forwardMax int64 = 5 forwardMax int64 = 5
// Объектики клиента-редиски. // RedisClient объектик клиента-редиски.
var RedisClient *redis.Client RedisClient *redis.Client
var Subscriber *redis.PubSub
// Main context. // Subscriber is redis PubSub object.
var ctx = context.Background() Subscriber *redis.PubSub
// Ставится в true, если мы получили сигнал на выключение. // Ctx is main context.
var Shutdown = false Ctx = context.Background()
// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. // Shutdown ставится в true, если мы получили сигнал на выключение.
var SigChan = make(chan os.Signal, 1) Shutdown = false
// Мапка с открытыми дескрипторами баз с настройками. // SigChan канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов.
var settingsDB = make(map[string]*pebble.DB) SigChan = make(chan os.Signal, 1)
var tg echotron.API // settingsDB мапка с открытыми дескрипторами баз с настройками.
settingsDB = make(map[string]*pebble.DB)
// Приветсвенные фразы для новых участников чата. // chatListDB объектик *pebble.DB с базой, в которой лежит список чатов.
var introduceGreet = [...]string{ 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{
"Дратути", "Дратути",
"Дарована", "Дарована",
"Доброе утро, день или вечер", "Доброе утро, день или вечер",
@@ -42,4 +59,8 @@ var 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: */ /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -10,9 +10,10 @@ import (
// Help Выводит в чат сообщение с основными командами бота. // Help Выводит в чат сообщение с основными командами бота.
func Help(cmd *echotron.Update) (bool, error) { func Help(cmd *echotron.Update) (bool, error) {
help := "```\n" help := "```\n"
help += fmt.Sprintf("%shelp | %sпомощь - список команд", Config.Csign, Config.Csign) help += fmt.Sprintf("%shelp | %sпомощь - список команд", Config.Csign, Config.Csign)
help += fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", Config.Csign, Config.Csign, Config.Csign) help += fmt.Sprintf("%sanek | %sанек | %sанекдот - рандомный анекдот с anekdot.ru", Config.Csign, Config.Csign, Config.Csign)
help += fmt.Sprintf("%sbuni - рандомный стрип hapi buni", Config.Csign) help += Config.Csign + "buni - рандомный стрип hapi buni"
help += fmt.Sprintf("%sbunny | %srabbit | %sкролик - кролик", Config.Csign, Config.Csign, Config.Csign) help += fmt.Sprintf("%sbunny | %srabbit | %sкролик - кролик", Config.Csign, Config.Csign, Config.Csign)
help += fmt.Sprintf("%scat | %sкис - кошечка", 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("%scoin | %sмонетка - подбросить монетку - орёл или решка?", Config.Csign, Config.Csign)
@@ -27,7 +28,7 @@ func Help(cmd *echotron.Update) (bool, error) {
help += fmt.Sprintf("%sfrog | %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("%shorse | %sлошадка - лошадка", Config.Csign, Config.Csign)
help += fmt.Sprintf("%slat | %sлат - сгенерить фразу из крылатых латинских выражений", Config.Csign, Config.Csign) help += fmt.Sprintf("%slat | %sлат - сгенерить фразу из крылатых латинских выражений", Config.Csign, Config.Csign)
help += fmt.Sprintf("%smonkeyuser - рандомный стрип MonkeyUser", Config.Csign) help += Config.Csign + "monkeyuser - рандомный стрип MonkeyUser"
help += fmt.Sprintf("%sowl | %sсова | %sсыч - сова", Config.Csign, Config.Csign, Config.Csign) help += fmt.Sprintf("%sowl | %sсова | %sсыч - сова", Config.Csign, Config.Csign, Config.Csign)
help += fmt.Sprintf("%sproverb | %sпословица - рандомная русская пословица", 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("%sping | %sпинг - попинговать бота", Config.Csign, Config.Csign)
@@ -35,11 +36,11 @@ func Help(cmd *echotron.Update) (bool, error) {
help += fmt.Sprintf("%ssnail | %sулитка - улитка", 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("%sver | %sversion | %sверсия - что-то про версию ПО", Config.Csign, Config.Csign, Config.Csign)
help += fmt.Sprintf("%sw <город> | %sп <город> - погода в указанном городе", Config.Csign, Config.Csign) help += fmt.Sprintf("%sw <город> | %sп <город> - погода в указанном городе", Config.Csign, Config.Csign)
help += fmt.Sprintf("%sweather <город> - погода в указанном городе", Config.Csign) help += Config.Csign + "weather <город> - погода в указанном городе"
help += fmt.Sprintf("%sпогода <город> - погода в указанном городе", Config.Csign) help += Config.Csign + "погода <город> - погода в указанном городе"
help += fmt.Sprintf("%sпогодка <город> - погода в указанном городе", Config.Csign) help += Config.Csign + "погодка <город> - погода в указанном городе"
help += fmt.Sprintf("%sпогадка <город> - погода в указанном городе", Config.Csign) help += Config.Csign + "погадка <город> - погода в указанном городе"
help += fmt.Sprintf("%sxkcd - рандомный стрип с сайта xkcd.ru", Config.Csign) help += Config.Csign + "xkcd - рандомный стрип с сайта xkcd.ru"
help += fmt.Sprintf("%skarma фраза | %sкарма фраза - посмотреть карму фразы", Config.Csign, Config.Csign) help += fmt.Sprintf("%skarma фраза | %sкарма фраза - посмотреть карму фразы", Config.Csign, Config.Csign)
help += "```\n" help += "```\n"
help += "Но на самом деле я бот больше для общения, чем для исполнения команд.\n" help += "Но на самом деле я бот больше для общения, чем для исполнения команд.\n"

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

@@ -10,53 +10,53 @@ import (
"github.com/hjson/hjson-go" "github.com/hjson/hjson-go"
) )
// 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
} }
// Валидируем значения из конфига. // Валидируем значения из конфига.
@@ -73,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 {
@@ -103,7 +103,7 @@ func ParseConfig(path string) (myConfig, error) {
} }
if sampleConfig.DataDir == "" { if sampleConfig.DataDir == "" {
return myConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path) return MyConfig{}, fmt.Errorf("data_dir field in config file %s must be set", path)
} }
return sampleConfig, err return sampleConfig, err

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

@@ -26,7 +26,7 @@ func RedisMsgParser(msg string) {
return return
} }
// Validate our j // Validate our j.
if exist := j.From; exist == "" { if exist := j.From; exist == "" {
log.Warnf("Incorrect msg from redis, no from field: %s", msg) log.Warnf("Incorrect msg from redis, no from field: %s", msg)
@@ -63,7 +63,7 @@ func RedisMsgParser(msg string) {
return return
} }
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет // j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет.
if j.Misc.Answer == 0 { if j.Misc.Answer == 0 {
log.Debug("Field Misc->Answer = 0, skipping message") log.Debug("Field Misc->Answer = 0, skipping message")
@@ -76,14 +76,14 @@ func RedisMsgParser(msg string) {
j.Misc.Csign = Config.Csign j.Misc.Csign = Config.Csign
} }
// j.Misc.FwdCnt если нам его не передали, то будет 0 // j.Misc.FwdCnt если нам его не передали, то будет 0.
if exist := j.Misc.Fwdcnt; exist == 0 { if exist := j.Misc.Fwdcnt; exist == 0 {
j.Misc.Fwdcnt = 1 j.Misc.Fwdcnt = 1
} }
// j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0 // j.Misc.GoodMorning может быть 1 или 0, по-умолчанию 0.
// j.Misc.MsgFormat может быть 1 или 0, по-умолчанию 0 // j.Misc.MsgFormat может быть 1 или 0, по-умолчанию 0.
// j.Misc.Username можно не передавать, тогда будет пустая строка // j.Misc.Username можно не передавать, тогда будет пустая строка.
// Отвалидировались, теперь вернёмся к нашим баранам. // Отвалидировались, теперь вернёмся к нашим баранам.

View File

@@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"slices"
"strings"
"aleesa-telegram-go/internal/log" "aleesa-telegram-go/internal/log"
@@ -45,6 +47,96 @@ func FetchV(db *pebble.DB, key string) (string, error) {
return valueString, err return valueString, err
} }
// InitChatListDB открывает для пользования базу со списком чатов.
// Вторым слоем этой базы являются слайсы chatList и chatGroupList, по которым можно в дальнейшем определять, известен
// ли даденный чат боту. По сути, слайс chatList активно пользуется в функции AppendChatListDB, а chatGroupList - в
// SendGoodMorning.
func InitChatListDB() error {
var (
err error
options pebble.Options
dbPath = Config.DataDir + "/" + chatListDBName
)
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
options.L0CompactionFileThreshold = 8
chatListDB, err = pebble.Open(dbPath, &options)
if err != nil {
return fmt.Errorf("unable to open settings db, %s: %w", dbPath, err)
}
// Заполним слайс со списком чатов. Операция нудная.
iterator, err := chatListDB.NewIter(&pebble.IterOptions{})
if !iterator.First() {
return errors.New("unable to iterate slice with chats")
}
// Попробуем извлечь список ключей и заполнить слайс с чатами.
for !iterator.Valid() {
// Битая база?
if err := iterator.Error(); err != nil {
chatList = []string{}
return fmt.Errorf("unable to get chat list: %w", err)
}
chatName := iterator.Key()
if len(chatName) == 0 {
return errors.New("got empty chat name when query " + chatListDBName)
}
chatID := string(chatName)
chatList = append(chatList, chatID)
slices.Sort(chatList)
if strings.HasPrefix(chatID, "-") {
chatGroupList = append(chatGroupList, chatID)
slices.Sort(chatGroupList)
}
iterator.Next()
}
return err
}
// AppendChatListDB добавляет чат в список известных боту. Можно добавлять чаты не глядя, дубликатов не добавится.
// Bot API нам не даёт информацию о том, на что подписан бот.
func AppendChatListDB(chatID string) error {
var err error
if slices.Contains(chatList, chatID) {
return err
}
if err = StoreKV(chatListDB, chatID, "1"); err != nil {
return fmt.Errorf("unable to append chat %s to chatListDB %s: %w", chatID, chatListDBName, err)
}
// Не забываем актуализировать список чатов.
chatList = append(chatList, chatID)
slices.Sort(chatList)
chatList = slices.Compact(chatList)
// Если СhatID начинается с - это значит, что мы имеем дело именно с группой/супергруппой/каналом.
if strings.HasPrefix(chatID, "-") {
chatGroupList = append(chatGroupList, chatID)
slices.Sort(chatGroupList)
chatGroupList = slices.Compact(chatGroupList)
}
return err
}
// GetSetting достаёт настройку из БД с настройками. // GetSetting достаёт настройку из БД с настройками.
func GetSetting(chatID string, setting string) string { func GetSetting(chatID string, setting string) string {
var err error var err error
@@ -52,7 +144,7 @@ func GetSetting(chatID string, setting string) string {
chatHash := sha256.Sum256([]byte(chatID)) chatHash := sha256.Sum256([]byte(chatID))
database := fmt.Sprintf("settings_db/%x", chatHash) database := fmt.Sprintf("settings_db/%x", chatHash)
// Если БД не открыта, откроем её // Если БД не открыта, откроем её.
if _, ok := settingsDB[database]; !ok { if _, ok := settingsDB[database]; !ok {
var options pebble.Options var options pebble.Options
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными // По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
@@ -114,8 +206,14 @@ func SaveSetting(chatID string, setting string, value string) error {
} }
} }
if err := StoreKV(settingsDB[database], setting, value); err != nil { if err = StoreKV(settingsDB[database], setting, value); err != nil {
log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err) log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
return err
}
if err = settingsDB[database].Flush(); err != nil {
log.Errorf("Unable to flush settings db for %s: %s", chatID, err)
} }
return err return err

View File

@@ -30,7 +30,7 @@ func SigHandler() {
Shutdown = true Shutdown = true
// Отпишемся от всех каналов и закроем коннект к редиске. // Отпишемся от всех каналов и закроем коннект к редиске.
if err = Subscriber.Unsubscribe(ctx); err != nil { if err = Subscriber.Unsubscribe(Ctx); err != nil {
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err) log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
} else { } else {
log.Debug("Unsubscribe from all redis channels") log.Debug("Unsubscribe from all redis channels")
@@ -42,18 +42,12 @@ func SigHandler() {
log.Debug("Close redis connection") log.Debug("Close redis connection")
} }
// close all telego stuff. // Quit all scheduled periodic jobs.
resp, err := tg.LogOut() for _, job := range PeriodicJobs {
job.Quit <- true
if err != nil {
log.Errorf("Unable to send LogOut() to Telegram Bot API server")
} }
if !resp.Ok { resp, err := tg.Close()
log.Errorf("Telegram Bot API returns an error on LogOut(): %d, %s", resp.ErrorCode, resp.Description)
}
resp, err = tg.Close()
if err != nil { if err != nil {
log.Errorf("Unable to send Close() to Telegram Bot API server") log.Errorf("Unable to send Close() to Telegram Bot API server")
@@ -71,6 +65,10 @@ func SigHandler() {
} }
} }
log.Debug("Closing known chat list db")
_ = chatListDB.Close()
os.Exit(0) os.Exit(0)
} }
} }

View File

@@ -2,6 +2,7 @@ package tg
import ( import (
"fmt" "fmt"
"strconv"
"aleesa-telegram-go/internal/log" "aleesa-telegram-go/internal/log"
@@ -9,7 +10,7 @@ import (
) )
// Telega основная горутинка, реализующая бота. // Telega основная горутинка, реализующая бота.
func Telega(c myConfig) { func Telega(c MyConfig) {
tg = echotron.NewAPI(c.Telegram.Token) tg = echotron.NewAPI(c.Telegram.Token)
for u := range echotron.PollingUpdates(c.Telegram.Token) { for u := range echotron.PollingUpdates(c.Telegram.Token) {
@@ -74,10 +75,10 @@ func ConstructFullChatName(c *echotron.Chat) string {
func ConstructPartialUserUsername(u *echotron.User) string { func ConstructPartialUserUsername(u *echotron.User) string {
switch { switch {
case u.Username != "": case u.Username != "":
return fmt.Sprintf("@%s", u.Username) return "@" + u.Username
case u.FirstName != "" && u.LastName != "": case u.FirstName != "" && u.LastName != "":
return fmt.Sprintf("%s %s", u.FirstName, u.LastName) return u.FirstName + " " + u.LastName
case u.FirstName != "": case u.FirstName != "":
return u.FirstName return u.FirstName
@@ -86,19 +87,19 @@ func ConstructPartialUserUsername(u *echotron.User) string {
return u.LastName return u.LastName
default: default:
return fmt.Sprintf("%d", u.ID) return strconv.FormatInt(u.ID, 10)
} }
} }
// ConstructPartialUserUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если // ConstructPartialChatUsername пытается найти и вытащить username, если такового нет, вытаскивает First/Last Name, если
// такового нет, то возвращает ID. // такового нет, то возвращает ID.
func ConstructPartialChatUsername(c *echotron.Chat) string { func ConstructPartialChatUsername(c *echotron.Chat) string {
switch { switch {
case c.Username != "": case c.Username != "":
return fmt.Sprintf("@%s", c.Username) return "@" + c.Username
case c.FirstName != "" && c.LastName != "": case c.FirstName != "" && c.LastName != "":
return fmt.Sprintf("%s %s", c.FirstName, c.LastName) return c.FirstName + " " + c.LastName
case c.FirstName != "": case c.FirstName != "":
return c.FirstName return c.FirstName
@@ -107,7 +108,7 @@ func ConstructPartialChatUsername(c *echotron.Chat) string {
return c.LastName return c.LastName
default: default:
return fmt.Sprintf("%d", c.ID) return strconv.FormatInt(c.ID, 10)
} }
} }
@@ -118,15 +119,15 @@ func ConstructUserFirstLastName(u *echotron.User) string {
switch { switch {
case u.FirstName != "" && u.LastName != "": case u.FirstName != "" && u.LastName != "":
user = fmt.Sprintf("%s %s", u.FirstName, u.LastName) user = u.FirstName + " " + u.LastName
case u.FirstName != "": case u.FirstName != "":
user = u.FirstName user = u.FirstName
case u.LastName != "": case u.LastName != "":
user = u.LastName user = u.LastName
case u.Username != "": case u.Username != "":
user = fmt.Sprintf("@%s", u.Username) user = "@" + u.Username
default: default:
user = fmt.Sprintf("%d", u.ID) user = strconv.FormatInt(u.ID, 10)
} }
return user return user

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math/rand/v2" "math/rand/v2"
"regexp" "regexp"
"strconv"
"strings" "strings"
"aleesa-telegram-go/internal/log" "aleesa-telegram-go/internal/log"
@@ -20,11 +21,14 @@ func telegramMsgParser(msg *echotron.Update) {
return return
} }
if err := AppendChatListDB(strconv.FormatInt(msg.ChatID(), 10)); err != nil {
log.Error(err.Error())
}
// Сообщение о том, что этот чятик изменился, например, превратился в супергруппу. // Сообщение о том, что этот чятик изменился, например, превратился в супергруппу.
if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) { //nolint: revive,staticcheck if (msg.Message.MigrateFromChatID < 0) && (msg.Message.MigrateToChatID < 0) {
// TODO: поддержать миграцию настроек чата на новый chatId. // TODO: поддержать миграцию настроек чата на новый chatId.
// TODO: подумать, что можно сделать с настройками бэкэндов. Кажись, ничего, но надо глянуть. // TODO: подумать, что можно сделать с настройками бэкэндов. Кажись, ничего, но надо глянуть.
return return
} }
@@ -56,16 +60,17 @@ func telegramMsgParser(msg *echotron.Update) {
} }
// Люди пришли в чят. // Люди пришли в чят.
if msg.Message.NewChatMembers != nil { //nolint: revive,staticcheck if msg.Message.NewChatMembers != nil {
// TODO: проверить, надо ли приветствовать, выбрать одну из приветственных фраз, потянуть время, типа, мы пишем // TODO: проверить, надо ли приветствовать, выбрать одну из приветственных фраз, потянуть время, типа, мы пишем
// текст и поприветствовать вновь прибывшего. // текст и поприветствовать вновь прибывшего.
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GreetMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GreetMsg") == "1" {
// Пользователи, которых мы приветсвуем, одной строкой. // Пользователи, которых мы приветсвуем, одной строкой.
var users string var users string
if len(msg.Message.NewChatMembers) > 1 { if len(msg.Message.NewChatMembers) > 1 {
lastOne := msg.Message.NewChatMembers[len(msg.Message.NewChatMembers)-1] lastOne := msg.Message.NewChatMembers[len(msg.Message.NewChatMembers)-1]
theRest := msg.Message.NewChatMembers[:len(msg.Message.NewChatMembers)-1] theRest := msg.Message.NewChatMembers[:len(msg.Message.NewChatMembers)-1]
var usersSlice []string var usersSlice []string
for _, user := range theRest { for _, user := range theRest {
@@ -73,7 +78,8 @@ func telegramMsgParser(msg *echotron.Update) {
} }
users = strings.Join(usersSlice, ", ") users = strings.Join(usersSlice, ", ")
users += fmt.Sprintf(" and %s", ConstructTelegramHighlightName(lastOne))
users += " and " + ConstructTelegramHighlightName(lastOne)
} else { } else {
users = ConstructTelegramHighlightName(msg.Message.NewChatMembers[0]) users = ConstructTelegramHighlightName(msg.Message.NewChatMembers[0])
} }
@@ -88,7 +94,7 @@ func telegramMsgParser(msg *echotron.Update) {
resp, err := tg.SendChatAction( resp, err := tg.SendChatAction(
echotron.Typing, echotron.Typing,
msg.Message.From.ID, msg.Message.From.ID,
&echotron.ChatActionOptions{MessageThreadID: int(msg.Message.ThreadID)}, &echotron.ChatActionOptions{MessageThreadID: msg.Message.ThreadID},
) )
// TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или // TODO: вероятно, ошибку надо засылать в обработчик ошибок, так как в ней может быть миграция чятика или
@@ -123,10 +129,10 @@ func telegramMsgParser(msg *echotron.Update) {
// Человек ушёл из чата. // Человек ушёл из чата.
if msg.Message.LeftChatMember != nil { if msg.Message.LeftChatMember != nil {
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "GoodbyeMsg") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "GoodbyeMsg") == "1" {
user := ConstructUserFirstLastName(msg.Message.From) user := ConstructUserFirstLastName(msg.Message.From)
goodbye := fmt.Sprintf("Прощаемся с %s", user) goodbye := "Прощаемся с " + user
opts := echotron.MessageOptions{DisableNotification: true, ParseMode: "MarkdownV2"} opts := echotron.MessageOptions{DisableNotification: true, ParseMode: "MarkdownV2"}
@@ -160,8 +166,8 @@ func telegramMsgParser(msg *echotron.Update) {
rmsg.Misc.Answer = 1 rmsg.Misc.Answer = 1
// Засылаем фразу в misc-канал (в роутер). // Засылаем фразу в misc-канал (в роутер).
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID) rmsg.Chatid = strconv.FormatInt(msg.Message.Chat.ID, 10)
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID) rmsg.Userid = strconv.FormatInt(msg.Message.From.ID, 10)
rmsg.Message = msg.Message.Text rmsg.Message = msg.Message.Text
rmsg.Mode = "private" rmsg.Mode = "private"
rmsg.Plugin = "telegram" rmsg.Plugin = "telegram"
@@ -198,7 +204,7 @@ func telegramMsgParser(msg *echotron.Update) {
} }
// Заталкиваем наш json в редиску. // Заталкиваем наш json в редиску.
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil { 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) log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else { } else {
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data)) log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))
@@ -209,7 +215,7 @@ func telegramMsgParser(msg *echotron.Update) {
// Обрабатываем сообщения для групп и супергрупп. По сути это примерно одинаковые вещи/сущности. // Обрабатываем сообщения для групп и супергрупп. По сути это примерно одинаковые вещи/сущности.
// Цензор удаляет сообщения из чятика, если они "неправильные" - от имени других каналов, аудиосообщения, // Цензор удаляет сообщения из чятика, если они "неправильные" - от имени других каналов, аудиосообщения,
// содержат картинки, видео аудио итп, это настраивается через команду !admin censor. // содержат картинки, видео аудио итп, это настраивается через команду !admin censor.
if GetSetting(fmt.Sprintf("%d", msg.Message.Chat.ID), "censor") == "1" { if GetSetting(strconv.FormatInt(msg.Message.Chat.ID, 10), "censor") == "1" {
// Если цензор отработал, больше делать нечего, сообщения уже нету. // Если цензор отработал, больше делать нечего, сообщения уже нету.
if Censor(msg) { if Censor(msg) {
return return
@@ -239,7 +245,7 @@ func telegramMsgParser(msg *echotron.Update) {
return return
} }
// Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить // Здесь же, если фраза обращена к боту (ник или имя), выставляем флажок, что надо ответить.
quotedBotUsername := regexp.QuoteMeta(me.Result.Username) quotedBotUsername := regexp.QuoteMeta(me.Result.Username)
quotedBotFirstName := regexp.QuoteMeta(me.Result.FirstName) quotedBotFirstName := regexp.QuoteMeta(me.Result.FirstName)
quotedBotLastName := regexp.QuoteMeta(me.Result.LastName) quotedBotLastName := regexp.QuoteMeta(me.Result.LastName)
@@ -250,7 +256,9 @@ func telegramMsgParser(msg *echotron.Update) {
return return
} else if messageContainsBotUsername { } else if messageContainsBotUsername {
rmsg.Misc.Answer = 1 rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotUsername) m := regexp.MustCompile(quotedBotUsername)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "") msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
} }
@@ -273,7 +281,9 @@ func telegramMsgParser(msg *echotron.Update) {
if messageContainsBotName { if messageContainsBotName {
rmsg.Misc.Answer = 1 rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotFirstName + "[[:space:]]+" + quotedBotLastName) m := regexp.MustCompile(quotedBotFirstName + "[[:space:]]+" + quotedBotLastName)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "") msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
} }
// Если у нас есть только имя, то попробуем его вырезать из входящей фразы. // Если у нас есть только имя, то попробуем его вырезать из входящей фразы.
@@ -288,7 +298,9 @@ func telegramMsgParser(msg *echotron.Update) {
if messageContainsBotName { if messageContainsBotName {
rmsg.Misc.Answer = 1 rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotFirstName) m := regexp.MustCompile(quotedBotFirstName)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "") msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
} }
// Если у нас есть только фамилия, то попробуем его вырезать из входящей фразы. // Если у нас есть только фамилия, то попробуем его вырезать из входящей фразы.
@@ -303,14 +315,16 @@ func telegramMsgParser(msg *echotron.Update) {
if messageContainsBotName { if messageContainsBotName {
rmsg.Misc.Answer = 1 rmsg.Misc.Answer = 1
m := regexp.MustCompile(quotedBotLastName) m := regexp.MustCompile(quotedBotLastName)
msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "") msg.Message.Text = m.ReplaceAllString(msg.Message.Text, "")
} }
} }
// Засылаем фразу в misc-канал (в роутер). // Засылаем фразу в misc-канал (в роутер).
rmsg.Chatid = fmt.Sprintf("%d", msg.Message.Chat.ID) rmsg.Chatid = strconv.FormatInt(msg.Message.Chat.ID, 10)
rmsg.Userid = fmt.Sprintf("%d", msg.Message.From.ID) rmsg.Userid = strconv.FormatInt(msg.Message.From.ID, 10)
rmsg.Message = msg.Message.Text rmsg.Message = msg.Message.Text
rmsg.Mode = "public" rmsg.Mode = "public"
rmsg.Plugin = "telegram" rmsg.Plugin = "telegram"
@@ -335,7 +349,7 @@ func telegramMsgParser(msg *echotron.Update) {
} }
// Заталкиваем наш json в редиску. // Заталкиваем наш json в редиску.
if err := RedisClient.Publish(ctx, Config.Redis.Channel, data).Err(); err != nil { 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) log.Warnf("Unable to send data to redis channel %s: %s", Config.Redis.Channel, err)
} else { } else {
log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data)) log.Debugf("Sent msg to redis channel %s: %s", Config.Redis.Channel, string(data))

View File

@@ -1,27 +1,29 @@
package tg package tg
// myConfig структура, описывающая формат Конфига. type (
type myConfig struct { // MyConfig структура, описывающая формат Конфига.
Redis struct { MyConfig struct {
Server string `json:"server,omitempty"` Csign string `json:"csign,omitempty"`
Port int `json:"port,omitempty"` DataDir string `json:"data_dir,omitempty"`
Channel string `json:"channel,omitempty"` Log string `json:"log,omitempty"`
MyChannel string `json:"my_channel,omitempty"` Loglevel string `json:"loglevel,omitempty"`
} `json:"redis"`
Telegram struct { Telegram struct {
Token string `json:"token,omitempty"` 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"`
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
Csign string `json:"csign,omitempty"`
ForwardsMax int64 `json:"forwards_max,omitempty"` ForwardsMax int64 `json:"forwards_max,omitempty"`
DataDir string `json:"data_dir,omitempty"`
} }
// rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки. // rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки.
type rMsg struct { rMsg struct {
From string `json:"from,omitempty"` From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"` Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"` Userid string `json:"userid,omitempty"`
@@ -30,23 +32,23 @@ type rMsg struct {
Plugin string `json:"plugin,omitempty"` Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"` Mode string `json:"mode,omitempty"`
Misc struct { Misc struct {
Answer int64 `json:"answer,omitempty"`
Botnick string `json:"bot_nick,omitempty"` Botnick string `json:"bot_nick,omitempty"`
Csign string `json:"csign,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"` Fwdcnt int64 `json:"fwd_cnt,omitempty"`
GoodMorning int64 `json:"good_morning,omitempty"` GoodMorning int64 `json:"good_morning,omitempty"`
// В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ // В этом поле мы пишем 1, если бэкэнд должен в ответе форматировать сообщение в MarkdownV2, что какбэ
// не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда. // не оч хорошо, так как мы выносим особенности реализации бэкэнда сюда.
Msgformat int64 `json:"msg_format,omitempty"` Msgformat int64 `json:"msg_format,omitempty"`
// Судя по перловой реализации, сюда всегда попадает [$fullname](tg://user?id=$userid).
// Согласно докуметации, и перловой реализации бота, мы проставляем это поле тогда, когда мы точно знаем, что в
// бэкэнде оно используется.
Username string `json:"username,omitempty"`
} `json:"Misc"` } `json:"Misc"`
} }
// sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки. // sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки.
type sMsg struct { sMsg struct {
From string `json:"from"` From string `json:"from"`
Chatid string `json:"chatid"` Chatid string `json:"chatid"`
Userid string `json:"userid"` Userid string `json:"userid"`
@@ -55,14 +57,15 @@ type sMsg struct {
Plugin string `json:"plugin"` Plugin string `json:"plugin"`
Mode string `json:"mode"` Mode string `json:"mode"`
Misc struct { Misc struct {
Answer int64 `json:"answer"`
Botnick string `json:"bot_nick"` Botnick string `json:"bot_nick"`
Csign string `json:"csign"` Csign string `json:"csign"`
Username string `json:"username"`
Answer int64 `json:"answer"`
Fwdcnt int64 `json:"fwd_cnt"` Fwdcnt int64 `json:"fwd_cnt"`
GoodMorning int64 `json:"good_morning"` GoodMorning int64 `json:"good_morning"`
Msgformat int64 `json:"msg_format"` Msgformat int64 `json:"msg_format"`
Username string `json:"username"`
} `json:"misc"` } `json:"misc"`
} }
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

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

22
vendor/github.com/carlescere/scheduler/LICENSE generated vendored Normal file
View File

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

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
}

3
vendor/modules.txt vendored
View File

@@ -7,6 +7,9 @@ github.com/NicoNex/echotron/v3
# github.com/beorn7/perks v1.0.1 # github.com/beorn7/perks v1.0.1
## explicit; go 1.11 ## explicit; go 1.11
github.com/beorn7/perks/quantile github.com/beorn7/perks/quantile
# github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
## explicit
github.com/carlescere/scheduler
# github.com/cespare/xxhash/v2 v2.3.0 # github.com/cespare/xxhash/v2 v2.3.0
## explicit; go 1.11 ## explicit; go 1.11
github.com/cespare/xxhash/v2 github.com/cespare/xxhash/v2