Replace prazdnikisegodnyaru with celebratoday, small underhood fixes
- Replace prazdnikisegodnyaru with celebratoday as first is banned in Russia - Update .golangci.yml rule set - Megatons of fixes for linter - Update README.md
This commit is contained in:
715
.golangci.yml
715
.golangci.yml
@@ -1,9 +1,14 @@
|
|||||||
version: "2"
|
version: "2"
|
||||||
run:
|
run:
|
||||||
go: "1.22"
|
go: "1.25"
|
||||||
modules-download-mode: readonly
|
modules-download-mode: readonly
|
||||||
tests: false
|
tests: false
|
||||||
allow-parallel-runners: true
|
allow-parallel-runners: true
|
||||||
|
issues:
|
||||||
|
# Maximum issues count per one linter. Set to 0 to disable (default 50)
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
# Maximum count of issues with the same text. Set to 0 to disable (default 3)
|
||||||
|
max-same-issues: 0
|
||||||
linters:
|
linters:
|
||||||
default: none
|
default: none
|
||||||
enable:
|
enable:
|
||||||
@@ -11,27 +16,33 @@ linters:
|
|||||||
- asciicheck
|
- asciicheck
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- containedctx
|
- containedctx
|
||||||
- copyloopvar
|
- contextcheck
|
||||||
- decorder
|
- decorder
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- errcheck
|
- errcheck
|
||||||
- errchkjson
|
- errchkjson
|
||||||
- errorlint
|
- errorlint
|
||||||
- exhaustive
|
- exhaustive
|
||||||
|
- exptostd
|
||||||
- ginkgolinter
|
- ginkgolinter
|
||||||
- gocheckcompilerdirectives
|
- gocheckcompilerdirectives
|
||||||
- gocritic
|
- gocritic
|
||||||
|
- godoclint
|
||||||
- godot
|
- godot
|
||||||
- goheader
|
- godox
|
||||||
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
|
- gosmopolitan
|
||||||
- govet
|
- govet
|
||||||
- grouper
|
- grouper
|
||||||
- iface
|
|
||||||
- importas
|
- importas
|
||||||
- inamedparam
|
|
||||||
- ineffassign
|
- ineffassign
|
||||||
|
- intrange
|
||||||
- ireturn
|
- ireturn
|
||||||
- makezero
|
- makezero
|
||||||
|
- mirror
|
||||||
|
- misspell
|
||||||
|
- modernize
|
||||||
- nakedret
|
- nakedret
|
||||||
- nilerr
|
- nilerr
|
||||||
- nilnil
|
- nilnil
|
||||||
@@ -45,19 +56,690 @@ linters:
|
|||||||
- reassign
|
- reassign
|
||||||
- revive
|
- revive
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sloglint
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- unparam
|
|
||||||
- unused
|
- unused
|
||||||
|
- usestdlibvars
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
- wsl_v5
|
- wsl_v5
|
||||||
settings:
|
settings:
|
||||||
gosec:
|
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:
|
excludes:
|
||||||
- G404
|
# Боже, как же вы затрахали своей крипто-ебучкой, наркоманы хреновы!
|
||||||
- G402
|
- 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:
|
exclusions:
|
||||||
generated: lax
|
generated: lax
|
||||||
presets:
|
presets:
|
||||||
@@ -79,3 +761,16 @@ formatters:
|
|||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- builtin$
|
||||||
- examples$
|
- examples$
|
||||||
|
settings:
|
||||||
|
gofmt:
|
||||||
|
# Simplify code: gofmt with `-s` option.
|
||||||
|
# Default: true
|
||||||
|
simplify: true
|
||||||
|
# Apply the rewrite rules to the source before reformatting.
|
||||||
|
# https://pkg.go.dev/cmd/gofmt
|
||||||
|
# Default: []
|
||||||
|
rewrite-rules:
|
||||||
|
- pattern: 'interface{}'
|
||||||
|
replacement: 'any'
|
||||||
|
- pattern: 'a[b:len(a)]'
|
||||||
|
replacement: 'a[b:]'
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## Как это собрать?
|
## Как это собрать?
|
||||||
|
|
||||||
Понадобится компилятор go версии 1.25.1 или более новый.
|
Понадобится компилятор go версии 1.25.3 или более новый.
|
||||||
|
|
||||||
Сам процесс сборки простой, достаточно запустить make или gmake, в зависимости от платформы:
|
Сам процесс сборки простой, достаточно запустить make или gmake, в зависимости от платформы:
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
и через некоторое время мы получаем бинарник **aleesa-irc-go**. Это и есть наше приложение.
|
и через некоторое время мы получаем бинарник **aleesa-webapp-go**. Это и есть наше приложение.
|
||||||
|
|
||||||
### Как это запустить?
|
### Как это запустить?
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Implements aleesa-web-go application.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
44
cmd/celebratoday_test/main.go
Normal file
44
cmd/celebratoday_test/main.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Package main implements celebratoday api test.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"aleesa-webapp-go/internal/celebratoday"
|
||||||
|
"aleesa-webapp-go/internal/log"
|
||||||
|
"aleesa-webapp-go/internal/webapp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
logfile *os.File
|
||||||
|
)
|
||||||
|
|
||||||
|
err := webapp.ReadConfig()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to parse config: %s", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Откроем лог и скормим его логгеру.
|
||||||
|
if webapp.Config.Log != "" {
|
||||||
|
logfile, err = os.OpenFile(webapp.Config.Log, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Unable to open log file %s: %s", webapp.Config.Log, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Init(webapp.Config.Loglevel, logfile)
|
||||||
|
|
||||||
|
str, err := celebratoday.APIClient(webapp.Config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(str)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Implements initialisation app for flickr service. Gains Tokens, etc.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package main get flickr oauth keys required for proper client work an stores it locally.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package main ensures that oauth keys downloaded correctly and client part is working.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package anekdotru implemets client for anekdot.ru service.
|
||||||
package anekdotru
|
package anekdotru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -23,11 +24,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
url = "https://www.anekdot.ru/rss/randomu.html"
|
url = "https://www.anekdot.ru/rss/randomu.html"
|
||||||
)
|
)
|
||||||
|
|
||||||
err := requests.
|
err := requests.URL(url).UserAgent(userAgent).ToBytesBuffer(&respBody).Fetch(ctx)
|
||||||
URL(url).
|
|
||||||
UserAgent(userAgent).
|
|
||||||
ToBytesBuffer(&respBody).
|
|
||||||
Fetch(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
||||||
@@ -42,6 +39,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
|
|
||||||
if (len(aneks) >= 2) && (aneks[1] != "") {
|
if (len(aneks) >= 2) && (aneks[1] != "") {
|
||||||
re = regexp.MustCompile(`'\);`)
|
re = regexp.MustCompile(`'\);`)
|
||||||
|
|
||||||
a := re.Split(aneks[1], 2)
|
a := re.Split(aneks[1], 2)
|
||||||
|
|
||||||
if (len(a) >= 2) && (a[0] != "") {
|
if (len(a) >= 2) && (a[0] != "") {
|
||||||
@@ -51,7 +49,9 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
anek = re.ReplaceAll(anek, []byte(`"`))
|
anek = re.ReplaceAll(anek, []byte(`"`))
|
||||||
|
|
||||||
re = regexp.MustCompile(`\\"`)
|
re = regexp.MustCompile(`\\"`)
|
||||||
|
|
||||||
anekJSON := re.ReplaceAll(anek, []byte(`"`))
|
anekJSON := re.ReplaceAll(anek, []byte(`"`))
|
||||||
|
|
||||||
anekJSON = bytes.ToValidUTF8(anekJSON, []byte{0xef, 0xbf, 0xbd})
|
anekJSON = bytes.ToValidUTF8(anekJSON, []byte{0xef, 0xbf, 0xbd})
|
||||||
|
|
||||||
var anekMap []string
|
var anekMap []string
|
||||||
@@ -60,6 +60,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
re = regexp.MustCompile(`<br>`)
|
re = regexp.MustCompile(`<br>`)
|
||||||
|
|
||||||
answer := re.ReplaceAll([]byte(anekMap[0]), []byte("\n"))
|
answer := re.ReplaceAll([]byte(anekMap[0]), []byte("\n"))
|
||||||
|
|
||||||
return string(answer), nil
|
return string(answer), nil
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package bunicomic implements client for bunicomic.com service.
|
||||||
package bunicomic
|
package bunicomic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -48,13 +49,13 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
if n.Type == html.ElementNode && n.Data == "meta" {
|
if n.Type == html.ElementNode && n.Data == "meta" {
|
||||||
for _, meta := range n.Attr {
|
for _, meta := range n.Attr {
|
||||||
if meta.Key == "content" {
|
if meta.Key == "content" {
|
||||||
// adds a new link entry when the attribute matches
|
// Adds a new link entry when the attribute matches.
|
||||||
metaTags = append(metaTags, meta.Val)
|
metaTags = append(metaTags, meta.Val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// traverses the HTML of the webpage from the first child node
|
// Traverses the HTML of the webpage from the first child node.
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
meta(c)
|
meta(c)
|
||||||
}
|
}
|
||||||
|
|||||||
223
internal/celebratoday/celebratoday.go
Normal file
223
internal/celebratoday/celebratoday.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
// Package celebratoday implements client for celebratoday.com service.
|
||||||
|
package celebratoday
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aleesa-webapp-go/internal/config"
|
||||||
|
"aleesa-webapp-go/internal/log"
|
||||||
|
"aleesa-webapp-go/internal/pcachedb"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/carlmjohnson/requests"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client обёртка для клиента сервиса celebratoday.com, работающая с кэшем. Запросы надо делать
|
||||||
|
// именно через неё.
|
||||||
|
func Client(cfg *config.MyConfig) (string, error) {
|
||||||
|
tsNow := time.Now()
|
||||||
|
|
||||||
|
// eTime TTL кэша. На текущий момент +1 час к текущему времени.
|
||||||
|
eTime := tsNow.Unix() + 3600
|
||||||
|
|
||||||
|
tsStr := pcachedb.GetValue(cfg, "cache", "celebratoday_timestamp")
|
||||||
|
|
||||||
|
// Кэш пустой, запросов пока не было.
|
||||||
|
if tsStr == "" {
|
||||||
|
log.Debugf("CT no cache entry for celebratoday.com")
|
||||||
|
|
||||||
|
answer, err := APIClient(cfg)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := UpdateCache(cfg, eTime, answer); err != nil {
|
||||||
|
log.Errorf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsCache, err := strconv.ParseInt(tsStr, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("KSP cache miss")
|
||||||
|
log.Warnf("KSP-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
|
||||||
|
|
||||||
|
answer, err := APIClient(cfg)
|
||||||
|
|
||||||
|
// Бинго! у нас ещё и ошибка при обращении к "api" celebratoday.com, здесь мы ничего сделать не можем.
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если всё хорошо, надо обновить кэш.
|
||||||
|
if err := UpdateCache(cfg, eTime, answer); err != nil {
|
||||||
|
log.Errorf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Текущее время в Unix timestamp.
|
||||||
|
tsNowUnix := tsNow.Unix()
|
||||||
|
|
||||||
|
// Кэш просрочен.
|
||||||
|
if tsCache < tsNowUnix {
|
||||||
|
log.Debug("KSP cache miss")
|
||||||
|
|
||||||
|
answer, err := APIClient(cfg)
|
||||||
|
|
||||||
|
// Здесь мы ничего сделать не можем.
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если всё хорошо, надо обновить кэш.
|
||||||
|
if err := UpdateCache(cfg, eTime, answer); err != nil {
|
||||||
|
log.Errorf("%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше.
|
||||||
|
// TODO: Научиться определять ситуацию, когда значением является unix timestamp.
|
||||||
|
log.Debug("CT cache hit")
|
||||||
|
|
||||||
|
return pcachedb.GetValue(cfg, "cache", "celebratoday_value"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIClient клиент сервиса https://celebratoday.com/.
|
||||||
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
url = "https://celebratoday.com/ru/today"
|
||||||
|
respBody string
|
||||||
|
orangeClass int8
|
||||||
|
tgHref int8
|
||||||
|
)
|
||||||
|
|
||||||
|
// Формальная попытка притвориться браузером.
|
||||||
|
_, _, day := time.Now().Date()
|
||||||
|
userAgent := cfg.UserAgents[day]
|
||||||
|
|
||||||
|
if err := requests.URL(url).UserAgent(userAgent).
|
||||||
|
Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8").
|
||||||
|
Header("Accept-Language", "en-US,en;q=0.5").
|
||||||
|
Header("User-Agent", "ru-RU").
|
||||||
|
Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var holidays []string
|
||||||
|
|
||||||
|
// Парсим body ответа от сервера.
|
||||||
|
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
|
||||||
|
|
||||||
|
for {
|
||||||
|
tokenType := tokenizer.Next()
|
||||||
|
|
||||||
|
if tokenType == html.ErrorToken {
|
||||||
|
err := tokenizer.Err()
|
||||||
|
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
// End of the file, break out of the loop.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenType == html.StartTagToken {
|
||||||
|
token := tokenizer.Token()
|
||||||
|
|
||||||
|
if token.Data == "a" {
|
||||||
|
for _, attribute := range token.Attr {
|
||||||
|
// Кнопка о том, что у нас было вчера.
|
||||||
|
if attribute.Key == "class" && attribute.Val == "text-orange-500 underline hover:no-underline" {
|
||||||
|
orangeClass++
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Наши ссылки находятся после первого и до второго упоминания о телеграме.
|
||||||
|
if attribute.Key == "href" && attribute.Val == "https://t.me/celebratoday" {
|
||||||
|
tgHref++
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if attribute.Key == "title" && tgHref == 1 && orangeClass == 2 {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(attribute.Val, "Праздники "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
case strings.HasPrefix(attribute.Val, "Международные праздники "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
case strings.HasPrefix(attribute.Val, "Всемирные праздники "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
case strings.HasPrefix(attribute.Val, "Спортивные праздники "):
|
||||||
|
continue
|
||||||
|
|
||||||
|
case strings.HasPrefix(attribute.Val, "Медицинские праздники"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
case strings.HasPrefix(attribute.Val, "Подробнее про"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
default:
|
||||||
|
holidays = append(holidays, strings.TrimSuffix(attribute.Val, " | celebratoday.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если мы встретили вторую ссылку на телеграм, то конечная, дальше нам не надо.
|
||||||
|
if tgHref >= 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(holidays) == 0 {
|
||||||
|
return "", errors.New("unable to parse response from celebratoday.com: no holidays found")
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(holidays)
|
||||||
|
|
||||||
|
holidays = slices.Compact(holidays)
|
||||||
|
|
||||||
|
return "* " + strings.Join(holidays, "\n* "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCache сохраняет в кэш ответ от сервиса celebratoday и временной штамп.
|
||||||
|
func UpdateCache(cfg *config.MyConfig, eTime int64, value string) error {
|
||||||
|
key := "celebratoday_timestamp"
|
||||||
|
timestamp := strconv.FormatInt(eTime, 10)
|
||||||
|
|
||||||
|
if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, timestamp); err != nil {
|
||||||
|
return fmt.Errorf("CT-Cache: unable to save timestamp to cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key = "celebratoday_value"
|
||||||
|
|
||||||
|
if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, value); err != nil {
|
||||||
|
return fmt.Errorf("CT-Cache: unable to save answer to cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Этот package нужен, чтобы разбить кольцевую зависимость.
|
// Package config нужен, чтобы разбить кольцевую зависимость.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,32 +6,34 @@ import (
|
|||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MyConfig структурка, описывающая конфиг.
|
type (
|
||||||
type MyConfig struct {
|
// MyConfig структурка, описывающая конфиг.
|
||||||
Server string `json:"server,omitempty"`
|
MyConfig struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Port int `json:"port,omitempty"`
|
Server string `json:"server,omitempty"`
|
||||||
Timeout int `json:"timeout,omitempty"`
|
Port int `json:"port,omitempty"`
|
||||||
Loglevel string `json:"loglevel,omitempty"`
|
Timeout int `json:"timeout,omitempty"`
|
||||||
Log string `json:"log,omitempty"`
|
Loglevel string `json:"loglevel,omitempty"`
|
||||||
Channel string `json:"channel,omitempty"`
|
Log string `json:"log,omitempty"`
|
||||||
DataDir string `json:"datadir,omitempty"`
|
Channel string `json:"channel,omitempty"`
|
||||||
Csign string `json:"csign,omitempty"`
|
DataDir string `json:"datadir,omitempty"`
|
||||||
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
Csign string `json:"csign,omitempty"`
|
||||||
OpenWeatherMap struct {
|
ForwardsMax int64 `json:"forwards_max,omitempty"`
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
OpenWeatherMap struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Country bool `json:"country,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Appid string `json:"appid,omitempty"`
|
Country bool `json:"country,omitempty"`
|
||||||
} `json:"openweathermap,omitempty"`
|
Appid string `json:"appid,omitempty"`
|
||||||
Flickr struct {
|
} `json:"openweathermap"`
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Flickr struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Key string `json:"key,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Secret string `json:"secret,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
OAuthToken string `json:"oauth_token,omitempty"`
|
Secret string `json:"secret,omitempty"`
|
||||||
OAuthTokenSecret string `json:"oauth_token_secret,omitempty"`
|
OAuthToken string `json:"oauth_token,omitempty"`
|
||||||
} `json:"flickr,omitempty"`
|
OAuthTokenSecret string `json:"oauth_token_secret,omitempty"`
|
||||||
UserAgents []string `json:"user_agents,omitempty"`
|
} `json:"flickr"`
|
||||||
PcacheDB map[string]*pebble.DB
|
UserAgents []string `json:"user_agents,omitempty"`
|
||||||
RedisClient *redis.Client
|
PcacheDB map[string]*pebble.DB
|
||||||
}
|
RedisClient *redis.Client
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package flickr implements flickr.com service client.
|
||||||
package flickr
|
package flickr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -17,27 +18,29 @@ import (
|
|||||||
"gopkg.in/masci/flickr.v3/test"
|
"gopkg.in/masci/flickr.v3/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SearchResult структура, описывающая ответ api на поисковой запрос.
|
type (
|
||||||
type SearchResult struct {
|
// SearchResult структура, описывающая ответ api на поисковой запрос.
|
||||||
Photos struct {
|
SearchResult struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Page int64 `json:"page,omitempty"`
|
Photos struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Pages int64 `json:"pages,omitempty"`
|
Page int64 `json:"page,omitempty"`
|
||||||
Perpage int64 `json:"perpage,omitempty"`
|
Pages int64 `json:"pages,omitempty"`
|
||||||
Total int64 `json:"total,omitempty"`
|
Perpage int64 `json:"perpage,omitempty"`
|
||||||
Photo []struct {
|
Total int64 `json:"total,omitempty"`
|
||||||
Farm int64 `json:"farm,omitempty"`
|
Photo []struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
ID string `json:"id,omitempty"`
|
Farm int64 `json:"farm,omitempty"`
|
||||||
Isfamily int64 `json:"isfamily,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Isfriend int64 `json:"isfriend,omitempty"`
|
Isfamily int64 `json:"isfamily,omitempty"`
|
||||||
Ispublic int64 `json:"ispublic,omitempty"`
|
Isfriend int64 `json:"isfriend,omitempty"`
|
||||||
Owner string `json:"owner,omitempty"`
|
Ispublic int64 `json:"ispublic,omitempty"`
|
||||||
Secret string `json:"secret,omitempty"`
|
Owner string `json:"owner,omitempty"`
|
||||||
Server string `json:"server,omitempty"`
|
Secret string `json:"secret,omitempty"`
|
||||||
Title string `json:"title,omitempty"`
|
Server string `json:"server,omitempty"`
|
||||||
} `json:"photo,omitempty"`
|
Title string `json:"title,omitempty"`
|
||||||
} `json:"photos,omitempty"`
|
} `json:"photo"`
|
||||||
Stat string `json:"stat,omitempty"`
|
} `json:"photos"`
|
||||||
}
|
Stat string `json:"stat,omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// APIClientInit генерит и сохраняет token и token secret с помощью даденных пользователем key и secret.
|
// APIClientInit генерит и сохраняет token и token secret с помощью даденных пользователем key и secret.
|
||||||
// В процессе выдаём юзеру в консоль урл и запрос открыть его в браузере, чтобы получить verifier key, который нам
|
// В процессе выдаём юзеру в консоль урл и запрос открыть его в браузере, чтобы получить verifier key, который нам
|
||||||
@@ -47,15 +50,15 @@ func APIClientInit(cfg *config.MyConfig) error {
|
|||||||
|
|
||||||
client := flickr.NewFlickrClient(cfg.Flickr.Key, cfg.Flickr.Secret)
|
client := flickr.NewFlickrClient(cfg.Flickr.Key, cfg.Flickr.Secret)
|
||||||
|
|
||||||
// Первым делом достанем request token
|
// Первым делом достанем request token.
|
||||||
tok, err := flickr.GetRequestToken(client)
|
tok, err := flickr.GetRequestToken(client)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get Request Token: %w", err)
|
return fmt.Errorf("unable to get Request Token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Соорудим авторизационный URL
|
// Соорудим авторизационный URL.
|
||||||
url, err := flickr.GetAuthorizeUrl(client, tok)
|
link, err := flickr.GetAuthorizeUrl(client, tok)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get auth URL: %w", err)
|
return fmt.Errorf("unable to get auth URL: %w", err)
|
||||||
@@ -63,11 +66,12 @@ func APIClientInit(cfg *config.MyConfig) error {
|
|||||||
|
|
||||||
// А теперь ручные действия пользователя.
|
// А теперь ручные действия пользователя.
|
||||||
// Выдаём ему url, который надо ввести в адресную строку браузера, в браузере сгенерится verifier code, который нам
|
// Выдаём ему url, который надо ввести в адресную строку браузера, в браузере сгенерится verifier code, который нам
|
||||||
// нужен, чтобы вынуть из flicker api oauth token, secret, access token
|
// нужен, чтобы вынуть из flicker api oauth token, secret, access token.
|
||||||
var oauthVerifier string
|
var oauthVerifier string
|
||||||
|
|
||||||
fmt.Println("Open your browser at this url:", url)
|
fmt.Println("Open your browser at this url:", link)
|
||||||
fmt.Print("Then, insert the code: ")
|
fmt.Print("Then, insert the code: ")
|
||||||
|
|
||||||
_, _ = fmt.Scanln(&oauthVerifier)
|
_, _ = fmt.Scanln(&oauthVerifier)
|
||||||
|
|
||||||
// Наконец, достаём Access Token и Aceess Token Secret и сохраняем их в базку на диске.
|
// Наконец, достаём Access Token и Aceess Token Secret и сохраняем их в базку на диске.
|
||||||
@@ -127,7 +131,8 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Заполняем поля для оаутха.
|
// Заполняем поля для оаутха.
|
||||||
c.Init() // c.EndpointUrl
|
c.Init()
|
||||||
|
|
||||||
c.OAuthToken = oauthToken
|
c.OAuthToken = oauthToken
|
||||||
c.OAuthTokenSecret = oauthSecret
|
c.OAuthTokenSecret = oauthSecret
|
||||||
|
|
||||||
@@ -152,12 +157,12 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
|
|||||||
// разметки, а во-вторых, этот самый flickr.DoGet() валится с segfault-ом, просто потому что. Схема не такая, на
|
// разметки, а во-вторых, этот самый flickr.DoGet() валится с segfault-ом, просто потому что. Схема не такая, на
|
||||||
// которую он рассчитан. Но нам повезло, что можно просто вытащить подписанный url и заправить его в свой
|
// которую он рассчитан. Но нам повезло, что можно просто вытащить подписанный url и заправить его в свой
|
||||||
// http-клиент.
|
// http-клиент.
|
||||||
url := c.GetUrl()
|
link := c.GetUrl()
|
||||||
|
|
||||||
var response SearchResult
|
var response SearchResult
|
||||||
|
|
||||||
err := requests.
|
err := requests.
|
||||||
URL(url).
|
URL(link).
|
||||||
UserAgent(userAgent).
|
UserAgent(userAgent).
|
||||||
ToJSON(&response).
|
ToJSON(&response).
|
||||||
Fetch(ctx)
|
Fetch(ctx)
|
||||||
@@ -179,10 +184,10 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
|
|||||||
// Подписываем запрос oauth-говном.
|
// Подписываем запрос oauth-говном.
|
||||||
c.OAuthSign()
|
c.OAuthSign()
|
||||||
|
|
||||||
url = c.GetUrl()
|
link = c.GetUrl()
|
||||||
|
|
||||||
err = requests.
|
err = requests.
|
||||||
URL(url).
|
URL(link).
|
||||||
UserAgent(userAgent).
|
UserAgent(userAgent).
|
||||||
ToJSON(&response).
|
ToJSON(&response).
|
||||||
Fetch(ctx)
|
Fetch(ctx)
|
||||||
@@ -200,10 +205,10 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
|
|||||||
|
|
||||||
var u SearchResult
|
var u SearchResult
|
||||||
|
|
||||||
for i := 0; i < len(response.Photos.Photo); i++ {
|
for i := range len(response.Photos.Photo) {
|
||||||
skip := false
|
skip := false
|
||||||
|
|
||||||
for j := 0; j < len(u.Photos.Photo); j++ {
|
for j := range len(u.Photos.Photo) {
|
||||||
if response.Photos.Photo[i].Title == u.Photos.Photo[j].Title {
|
if response.Photos.Photo[i].Title == u.Photos.Photo[j].Title {
|
||||||
skip = true
|
skip = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package log implements logging facility as golang slog module wrapper.
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -33,9 +34,9 @@ func Init(level string, fileDescriptor *os.File) {
|
|||||||
|
|
||||||
opts := &slog.HandlerOptions{
|
opts := &slog.HandlerOptions{
|
||||||
// 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 output
|
// to be able to replace any single attribute in the log 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
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ func Init(level string, fileDescriptor *os.File) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error passes message directly to slog.Error().
|
// Error passes message directly to [slog.Error()].
|
||||||
func Error(message string) {
|
func Error(message string) {
|
||||||
slog.Error(message)
|
slog.Error(message)
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,7 @@ func Errorf(format string, a ...any) {
|
|||||||
slog.Error(fmt.Sprintf(format, a...))
|
slog.Error(fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn passes message directly to slog.Warn().
|
// Warn passes message directly to [slog.Warn()].
|
||||||
func Warn(message string) {
|
func Warn(message string) {
|
||||||
slog.Warn(message)
|
slog.Warn(message)
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,7 @@ func Warnf(format string, a ...any) {
|
|||||||
slog.Warn(fmt.Sprintf(format, a...))
|
slog.Warn(fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info passes message directly to slog.Info().
|
// Info passes message directly to [slog.Info()].
|
||||||
func Info(message string) {
|
func Info(message string) {
|
||||||
slog.Info(message)
|
slog.Info(message)
|
||||||
}
|
}
|
||||||
@@ -92,7 +93,7 @@ func Infof(format string, a ...any) {
|
|||||||
slog.Info(fmt.Sprintf(format, a...))
|
slog.Info(fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug passes message directly to slog.Debug().
|
// Debug passes message directly to [slog.Debug()].
|
||||||
func Debug(message string) {
|
func Debug(message string) {
|
||||||
slog.Debug(message)
|
slog.Debug(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package monkeyuser implements client for service www.monkeyusers.com.
|
||||||
package monkeyuser
|
package monkeyuser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,11 +15,13 @@ import (
|
|||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MonkeyUsers описывает структуру json-массива ответа www.monkeyusers.com.
|
type (
|
||||||
type MonkeyUsers []struct {
|
// MonkeyUsers описывает структуру json-массива ответа www.monkeyusers.com.
|
||||||
Title string `json:"title"`
|
MonkeyUsers []struct {
|
||||||
URL string `json:"url"`
|
Title string `json:"title"`
|
||||||
}
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// APIClient клиент сервиса monkeyuser.com.
|
// APIClient клиент сервиса monkeyuser.com.
|
||||||
func APIClient(cfg *config.MyConfig) (string, error) {
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
@@ -75,7 +78,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
return "", fmt.Errorf("unable to GET %s: %w", pageURL, err)
|
return "", fmt.Errorf("unable to GET %s: %w", pageURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим body ответа от сервера
|
// Парсим body ответа от сервера.
|
||||||
doc, err := html.Parse(strings.NewReader(body))
|
doc, err := html.Parse(strings.NewReader(body))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -91,7 +94,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
if n.Type == html.ElementNode && n.Data == "img" {
|
if n.Type == html.ElementNode && n.Data == "img" {
|
||||||
for _, a_href := range n.Attr { //nolint: revive
|
for _, a_href := range n.Attr { //nolint: revive
|
||||||
// Если у нас class == "lazyload small-image", то после него ключ data-src должен содержать
|
// Если у нас class == "lazyload small-image", то после него ключ data-src должен содержать
|
||||||
// искомый relative url
|
// искомый relative url.
|
||||||
if a_href.Key == "class" && a_href.Val == "logo" {
|
if a_href.Key == "class" && a_href.Val == "logo" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -105,7 +108,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// traverses the HTML of the webpage from the first child node
|
// traverses the HTML of the webpage from the first child node.
|
||||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
link(c)
|
link(c)
|
||||||
}
|
}
|
||||||
@@ -113,7 +116,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
|
|
||||||
link(doc)
|
link(doc)
|
||||||
|
|
||||||
// Возвращаем ссылку на картинку buni comic
|
// Возвращаем ссылку на картинку buni comic.
|
||||||
if hyperlink != "" {
|
if hyperlink != "" {
|
||||||
return fmt.Sprintf("%s%s", baseURL, hyperlink), nil
|
return fmt.Sprintf("%s%s", baseURL, hyperlink), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package oboobs implements api.oboobs.ru service client.
|
||||||
package oboobs
|
package oboobs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,14 +10,16 @@ import (
|
|||||||
"github.com/carlmjohnson/requests"
|
"github.com/carlmjohnson/requests"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Oboobs структура, возвращаемая на запрос в oboobs.ru.
|
type (
|
||||||
type Oboobs struct {
|
// Oboobs структура, возвращаемая на запрос в oboobs.ru.
|
||||||
ID int `json:"id,omitempty"`
|
Oboobs struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Author string `json:"author,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Rank int `json:"rank,omitempty"`
|
Author string `json:"author,omitempty"`
|
||||||
Model string `json:"model,omitempty"`
|
Rank int `json:"rank,omitempty"`
|
||||||
Preview string `json:"preview"`
|
Model string `json:"model,omitempty"`
|
||||||
}
|
Preview string `json:"preview"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// APIClient клиент сервиса oboobs.ru.
|
// APIClient клиент сервиса oboobs.ru.
|
||||||
func APIClient(cfg *config.MyConfig) (string, error) {
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package obutts implements api.obutts.ru service client.
|
||||||
package obutts
|
package obutts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,14 +10,16 @@ import (
|
|||||||
"github.com/carlmjohnson/requests"
|
"github.com/carlmjohnson/requests"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Obutts структура, возвращаемая на запрос в obutts.ru.
|
type (
|
||||||
type Obutts struct {
|
// Obutts структура, возвращаемая на запрос в obutts.ru.
|
||||||
ID int `json:"id,omitempty"`
|
Obutts struct { //nolint: govet // Идите в пень, мне так удобнее.
|
||||||
Author string `json:"author,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
Rank int `json:"rank,omitempty"`
|
Author string `json:"author,omitempty"`
|
||||||
Model string `json:"model,omitempty"`
|
Rank int `json:"rank,omitempty"`
|
||||||
Preview string `json:"preview"`
|
Model string `json:"model,omitempty"`
|
||||||
}
|
Preview string `json:"preview"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// APIClient клиент сервиса obutts.ru.
|
// APIClient клиент сервиса obutts.ru.
|
||||||
func APIClient(cfg *config.MyConfig) (string, error) {
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package openweathermap implements openweatermap.org service client.
|
||||||
package openweathermap
|
package openweathermap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,14 +15,16 @@ import (
|
|||||||
owm "github.com/briandowns/openweathermap"
|
owm "github.com/briandowns/openweathermap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Структурка для передачи в обрабтчик запросов в openweathermap api.
|
type (
|
||||||
type OwmItem struct {
|
// OwmItem структурка для передачи в обрабтчик запросов в openweathermap api.
|
||||||
City string
|
OwmItem struct {
|
||||||
FakeName string
|
City string
|
||||||
Where string
|
FakeName string
|
||||||
Lon float64
|
Where string
|
||||||
Lat float64
|
Lon float64
|
||||||
}
|
Lat float64
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// OwmClient обёртка с кэшированием для openweatermap.org.
|
// OwmClient обёртка с кэшированием для openweatermap.org.
|
||||||
func OwmClient(cfg *config.MyConfig, city string, days uint32) (string, error) {
|
func OwmClient(cfg *config.MyConfig, city string, days uint32) (string, error) {
|
||||||
@@ -41,23 +44,29 @@ func OwmClient(cfg *config.MyConfig, city string, days uint32) (string, error) {
|
|||||||
|
|
||||||
// "Нормализуем" название города.
|
// "Нормализуем" название города.
|
||||||
city = strings.TrimSpace(city)
|
city = strings.TrimSpace(city)
|
||||||
|
|
||||||
firstLetter := strings.SplitN(city, "", 2)[0]
|
firstLetter := strings.SplitN(city, "", 2)[0]
|
||||||
|
|
||||||
city = fmt.Sprintf("%s%s", strings.ToUpper(firstLetter), city[1:])
|
city = fmt.Sprintf("%s%s", strings.ToUpper(firstLetter), city[1:])
|
||||||
city = strings.ToValidUTF8(city, "")
|
city = strings.ToValidUTF8(city, "")
|
||||||
|
|
||||||
switch city {
|
switch city {
|
||||||
case "Msk", "Default", "Dc", "Мск", "Dc-Universe":
|
case "Msk", "Default", "Dc", "Мск", "Dc-Universe":
|
||||||
item.City = "Москва"
|
item.City = "Москва"
|
||||||
|
|
||||||
case "Spb", "Спб":
|
case "Spb", "Спб":
|
||||||
item.City = "Санкт-Петербург"
|
item.City = "Санкт-Петербург"
|
||||||
|
|
||||||
case "Ект", "Ёбург", "Ебург", "Екат", "Ekt", "Eburg", "Ekat":
|
case "Ект", "Ёбург", "Ебург", "Екат", "Ekt", "Eburg", "Ekat":
|
||||||
item.City = "Екатеринбург"
|
item.City = "Екатеринбург"
|
||||||
|
|
||||||
case "Кудыкина гора":
|
case "Кудыкина гора":
|
||||||
// Координаты Килиманжаро (-3.066524619045895, 37.35588473583672).
|
// Координаты Килиманжаро (-3.066524619045895, 37.35588473583672).
|
||||||
item.Lat = -3.066524619045895
|
item.Lat = -3.066524619045895
|
||||||
item.Lon = 37.35588473583672
|
item.Lon = 37.35588473583672
|
||||||
item.FakeName = "Килиманжаро"
|
item.FakeName = "Килиманжаро"
|
||||||
item.Where = "на горе"
|
item.Where = "на горе"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
item.City = city
|
item.City = city
|
||||||
item.Where = "в городе"
|
item.Where = "в городе"
|
||||||
@@ -96,8 +105,8 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if item.City != "" {
|
if item.City != "" {
|
||||||
if err = w.CurrentByName(item.City); err != nil {
|
if err := w.CurrentByName(item.City); err != nil {
|
||||||
// TODO: extend error
|
// TODO: extend error.
|
||||||
return answer, err
|
return answer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +114,8 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) {
|
|||||||
} else {
|
} else {
|
||||||
coord := owm.Coordinates{Longitude: item.Lon, Latitude: item.Lat}
|
coord := owm.Coordinates{Longitude: item.Lon, Latitude: item.Lat}
|
||||||
|
|
||||||
if err = w.CurrentByCoordinates(&coord); err != nil {
|
if err := w.CurrentByCoordinates(&coord); err != nil {
|
||||||
// TODO: extend error
|
// TODO: extend error.
|
||||||
return answer, err
|
return answer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +300,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
|
|||||||
// Первое, что приходит в голову - коллизия хэшей sha256.
|
// Первое, что приходит в голову - коллизия хэшей sha256.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("OWM-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
|
log.Warnf("OWM-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
|
||||||
|
|
||||||
answer, err := OwmAPIClient(cfg, item)
|
answer, err := OwmAPIClient(cfg, item)
|
||||||
|
|
||||||
// Бинго! у нас ещё и ошибка при обращении к api openwethermap, здесь мы ничего сделать не можем.
|
// Бинго! у нас ещё и ошибка при обращении к api openwethermap, здесь мы ничего сделать не можем.
|
||||||
@@ -306,7 +316,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
|
|||||||
return answer, err
|
return answer, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Кэш просрочен
|
// Кэш просрочен.
|
||||||
if tsCache < tsNow {
|
if tsCache < tsNow {
|
||||||
log.Debugf("OWM cache miss for city: %s", key)
|
log.Debugf("OWM cache miss for city: %s", key)
|
||||||
|
|
||||||
@@ -326,6 +336,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
key += "+value"
|
key += "+value"
|
||||||
|
|
||||||
answer = pcachedb.GetValue(cfg, "cache", key)
|
answer = pcachedb.GetValue(cfg, "cache", key)
|
||||||
|
|
||||||
// Какие-то проблемы с кэшом - ts есть, но самого кэша нету. Более подробная инфа по идее уже попала в лог.
|
// Какие-то проблемы с кэшом - ts есть, но самого кэша нету. Более подробная инфа по идее уже попала в лог.
|
||||||
@@ -334,7 +345,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
|
|||||||
if len(answer) < 140 {
|
if len(answer) < 140 {
|
||||||
log.Debugf("OWM cache miss for city: %s", key)
|
log.Debugf("OWM cache miss for city: %s", key)
|
||||||
|
|
||||||
if len(answer) > 0 {
|
if answer != "" {
|
||||||
log.Errorf("OWM-Cache: record for city %s in cache is too short, less than 140 chars", key)
|
log.Errorf("OWM-Cache: record for city %s in cache is too short, less than 140 chars", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +384,7 @@ func UpdateOwmUserCache(cfg *config.MyConfig, chatid string, entry string, value
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Возвращает из кэша город, которым интересовался пользователь из даденного чятика.
|
// QueryOwmUserCache возвращает из кэша город, которым интересовался пользователь из даденного чятика.
|
||||||
func QueryOwmUserCache(cfg *config.MyConfig, chatid string, entry string) string {
|
func QueryOwmUserCache(cfg *config.MyConfig, chatid string, entry string) string {
|
||||||
// Chatid как есть лучше на диск не пробовать класть, мало ли что туда юзер впишет. Возьмём от него sha256.
|
// Chatid как есть лучше на диск не пробовать класть, мало ли что туда юзер впишет. Возьмём от него sha256.
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package pcachedb implements persistent cache using PebbleDB key-value library.
|
||||||
package pcachedb
|
package pcachedb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -51,12 +52,12 @@ func GetValue(cfg *config.MyConfig, cacheID string, setting string) string {
|
|||||||
cacheHash := sha256.Sum256([]byte(cacheID))
|
cacheHash := sha256.Sum256([]byte(cacheID))
|
||||||
database := fmt.Sprintf("cache_db/%x", cacheHash)
|
database := fmt.Sprintf("cache_db/%x", cacheHash)
|
||||||
|
|
||||||
// Если БД не открыта, откроем её
|
// Если БД не открыта, откроем её.
|
||||||
if _, ok := cfg.PcacheDB[database]; !ok {
|
if _, ok := cfg.PcacheDB[database]; !ok {
|
||||||
var options pebble.Options
|
var options pebble.Options
|
||||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||||
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
|
||||||
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений
|
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
|
||||||
options.L0CompactionFileThreshold = 8
|
options.L0CompactionFileThreshold = 8
|
||||||
cfg.PcacheDB[database], err = pebble.Open(cfg.DataDir+"/"+database, &options)
|
cfg.PcacheDB[database], err = pebble.Open(cfg.DataDir+"/"+database, &options)
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ func GetValue(cfg *config.MyConfig, cacheID string, setting string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveKeyWithValue сохраним данные в БД.
|
// SaveKeyWithValue сохраним данные в БД.
|
||||||
func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, value string) error {
|
func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, value string) error {
|
||||||
var (
|
var (
|
||||||
cacheHash = sha256.Sum256([]byte(cacheID))
|
cacheHash = sha256.Sum256([]byte(cacheID))
|
||||||
@@ -96,7 +97,7 @@ func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, valu
|
|||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// Если БД не открыта, откроем её
|
// Если БД не открыта, откроем её.
|
||||||
if _, ok := cfg.PcacheDB[database]; !ok {
|
if _, ok := cfg.PcacheDB[database]; !ok {
|
||||||
var options pebble.Options
|
var options pebble.Options
|
||||||
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
|
||||||
@@ -115,6 +116,12 @@ func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, valu
|
|||||||
|
|
||||||
if err := StoreKV(cfg.PcacheDB[database], setting, value); err != nil {
|
if err := StoreKV(cfg.PcacheDB[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 = cfg.PcacheDB[database].AsyncFlush(); err != nil {
|
||||||
|
log.Errorf("Unable to flush data to disk for database %s: %s", database, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package prazdnikisegodnyaru implements prazdniki-segodnya.ru service client.
|
||||||
|
// Now defunct on Russian Federation area for some legal reasons.
|
||||||
package prazdnikisegodnyaru
|
package prazdnikisegodnyaru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -20,14 +22,14 @@ import (
|
|||||||
func PsrClient(cfg *config.MyConfig) (string, error) {
|
func PsrClient(cfg *config.MyConfig) (string, error) {
|
||||||
tsNow := time.Now()
|
tsNow := time.Now()
|
||||||
|
|
||||||
// Вычислим expiration time для MSK+0 в формате unix timestamp
|
// Вычислим expiration time для MSK+0 в формате unix timestamp.
|
||||||
loc, _ := time.LoadLocation("Europe/Moscow")
|
loc, _ := time.LoadLocation("Europe/Moscow")
|
||||||
year, month, day := tsNow.In(loc).Date()
|
year, month, day := tsNow.In(loc).Date()
|
||||||
eTime := time.Date(year, month, day, 0, 0, 0, 0, loc).AddDate(0, 0, +1).Unix()
|
eTime := time.Date(year, month, day, 0, 0, 0, 0, loc).AddDate(0, 0, +1).Unix()
|
||||||
|
|
||||||
tsStr := pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_timestamp")
|
tsStr := pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_timestamp")
|
||||||
|
|
||||||
// Кэш пустой, запросов пока не было
|
// Кэш пустой, запросов пока не было.
|
||||||
if tsStr == "" {
|
if tsStr == "" {
|
||||||
log.Debugf("PSR no cache entry for prazdnikisegodhyaru")
|
log.Debugf("PSR no cache entry for prazdnikisegodhyaru")
|
||||||
|
|
||||||
@@ -49,14 +51,15 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("PSR cache miss")
|
log.Debug("PSR cache miss")
|
||||||
log.Warnf("PSR-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
|
log.Warnf("PSR-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
|
||||||
|
|
||||||
answer, err := PsrAPIClient(cfg)
|
answer, err := PsrAPIClient(cfg)
|
||||||
|
|
||||||
// Бинго! у нас ещё и ошибка при обращении к "api" prazdnikisegodhyaru, здесь мы ничего сделать не можем
|
// Бинго! у нас ещё и ошибка при обращении к "api" prazdnikisegodhyaru, здесь мы ничего сделать не можем.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если всё хорошо, надо обновить кэш
|
// Если всё хорошо, надо обновить кэш.
|
||||||
if err := UpdatePsrCache(cfg, eTime, answer); err != nil {
|
if err := UpdatePsrCache(cfg, eTime, answer); err != nil {
|
||||||
log.Errorf("%s", err)
|
log.Errorf("%s", err)
|
||||||
}
|
}
|
||||||
@@ -64,21 +67,21 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
|
|||||||
return answer, nil
|
return answer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Текущее время в Unix timestamp
|
// Текущее время в Unix timestamp.
|
||||||
tsNowUnix := tsNow.Unix()
|
tsNowUnix := tsNow.Unix()
|
||||||
|
|
||||||
// Кэш просрочен
|
// Кэш просрочен.
|
||||||
if tsCache < tsNowUnix {
|
if tsCache < tsNowUnix {
|
||||||
log.Debug("PSR cache miss")
|
log.Debug("PSR cache miss")
|
||||||
|
|
||||||
answer, err := PsrAPIClient(cfg)
|
answer, err := PsrAPIClient(cfg)
|
||||||
|
|
||||||
// Здесь мы ничего сделать не можем
|
// Здесь мы ничего сделать не можем.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если всё хорошо, надо обновить кэш
|
// Если всё хорошо, надо обновить кэш.
|
||||||
if err := UpdatePsrCache(cfg, eTime, answer); err != nil {
|
if err := UpdatePsrCache(cfg, eTime, answer); err != nil {
|
||||||
log.Errorf("%s", err)
|
log.Errorf("%s", err)
|
||||||
}
|
}
|
||||||
@@ -86,8 +89,8 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
|
|||||||
return answer, nil
|
return answer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше
|
// TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше.
|
||||||
// TODO: Научиться определять ситуацию, когда значением является unix timestamp
|
// TODO: Научиться определять ситуацию, когда значением является unix timestamp.
|
||||||
log.Debug("PSR cache hit")
|
log.Debug("PSR cache hit")
|
||||||
|
|
||||||
return pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_value"), nil
|
return pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_value"), nil
|
||||||
@@ -97,22 +100,22 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
|
|||||||
func PsrAPIClient(cfg *config.MyConfig) (string, error) {
|
func PsrAPIClient(cfg *config.MyConfig) (string, error) {
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
url = "https://prazdniki-segodnya.ru/"
|
link = "https://prazdniki-segodnya.ru/"
|
||||||
respBody string
|
respBody string
|
||||||
)
|
)
|
||||||
|
|
||||||
// Формальная попытка притвориться браузером
|
// Формальная попытка притвориться браузером.
|
||||||
_, _, day := time.Now().Date()
|
_, _, day := time.Now().Date()
|
||||||
userAgent := cfg.UserAgents[day]
|
userAgent := cfg.UserAgents[day]
|
||||||
|
|
||||||
if err := requests.URL(url).UserAgent(userAgent).Header("User-Agent", "ru-RU").
|
if err := requests.URL(link).UserAgent(userAgent).Header("User-Agent", "ru-RU").
|
||||||
Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil {
|
Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil {
|
||||||
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
return "", fmt.Errorf("unable to GET %s: %w", link, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var holidays []string
|
var holidays []string
|
||||||
|
|
||||||
// Парсим body ответа от сервера
|
// Парсим body ответа от сервера.
|
||||||
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
|
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -122,7 +125,7 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
err := tokenizer.Err()
|
err := tokenizer.Err()
|
||||||
|
|
||||||
if errors.Is(err, io.EOF) {
|
if errors.Is(err, io.EOF) {
|
||||||
// end of the file, break out of the loop
|
// end of the file, break out of the loop.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +142,7 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
|
|
||||||
if tokenType == html.TextToken {
|
if tokenType == html.TextToken {
|
||||||
str := tokenizer.Token().Data
|
str := tokenizer.Token().Data
|
||||||
|
|
||||||
holidays = append(holidays, strings.TrimSpace(str))
|
holidays = append(holidays, strings.TrimSpace(str))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package randomfox implements randomfox.ca service client.
|
||||||
package randomfox
|
package randomfox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,11 +11,13 @@ import (
|
|||||||
"github.com/carlmjohnson/requests"
|
"github.com/carlmjohnson/requests"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RandomFox структура, возвращаемая на запрос в randomfox.ca API.
|
type (
|
||||||
type RandomFox struct {
|
// RandomFox структура, возвращаемая на запрос в randomfox.ca API.
|
||||||
Image string `json:"image,omitempty"`
|
RandomFox struct {
|
||||||
Link string `json:"link"`
|
Image string `json:"image,omitempty"`
|
||||||
}
|
Link string `json:"link"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// APIClient клиент сервиса randomfox.ca.
|
// APIClient клиент сервиса randomfox.ca.
|
||||||
func APIClient(cfg *config.MyConfig) (string, error) {
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package thecatapi implements api.thecatapi.com service client.
|
||||||
package thecatapi
|
package thecatapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -9,16 +10,18 @@ import (
|
|||||||
"github.com/carlmjohnson/requests"
|
"github.com/carlmjohnson/requests"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TheCatAPIinnerStruct структурка, описывающая элемент массива, возвращемый Тhe cat api.
|
type (
|
||||||
type TheCatAPIinnerStruct struct {
|
// TheCatAPIinnerStruct структурка, описывающая элемент массива, возвращемый Тhe cat api.
|
||||||
ID string `json:"id,omitempty"`
|
TheCatAPIinnerStruct struct {
|
||||||
URL string `json:"url"`
|
ID string `json:"id,omitempty"`
|
||||||
Width int `json:"width,omitempty"`
|
URL string `json:"url"`
|
||||||
Height int `json:"height,omitempty"`
|
Width int `json:"width,omitempty"`
|
||||||
}
|
Height int `json:"height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API.
|
// TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API.
|
||||||
type TheCatAPI []TheCatAPIinnerStruct
|
TheCatAPI []TheCatAPIinnerStruct
|
||||||
|
)
|
||||||
|
|
||||||
// APIClient клиент сервиса thecatapi.com.
|
// APIClient клиент сервиса thecatapi.com.
|
||||||
func APIClient(cfg *config.MyConfig) (string, error) {
|
func APIClient(cfg *config.MyConfig) (string, error) {
|
||||||
@@ -29,11 +32,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
cat TheCatAPI
|
cat TheCatAPI
|
||||||
)
|
)
|
||||||
|
|
||||||
err := requests.
|
err := requests.URL(url).UserAgent(userAgent).ToJSON(&cat).Fetch(ctx)
|
||||||
URL(url).
|
|
||||||
UserAgent(userAgent).
|
|
||||||
ToJSON(&cat).
|
|
||||||
Fetch(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
return "", fmt.Errorf("unable to GET %s: %w", url, err)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
// Package webapp is main package implementing application logic.
|
||||||
package webapp
|
package webapp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"aleesa-webapp-go/internal/anekdotru"
|
"aleesa-webapp-go/internal/anekdotru"
|
||||||
"aleesa-webapp-go/internal/bunicomic"
|
"aleesa-webapp-go/internal/bunicomic"
|
||||||
|
"aleesa-webapp-go/internal/celebratoday"
|
||||||
"aleesa-webapp-go/internal/config"
|
"aleesa-webapp-go/internal/config"
|
||||||
"aleesa-webapp-go/internal/flickr"
|
"aleesa-webapp-go/internal/flickr"
|
||||||
"aleesa-webapp-go/internal/log"
|
"aleesa-webapp-go/internal/log"
|
||||||
@@ -10,7 +12,6 @@ import (
|
|||||||
"aleesa-webapp-go/internal/oboobs"
|
"aleesa-webapp-go/internal/oboobs"
|
||||||
"aleesa-webapp-go/internal/obutts"
|
"aleesa-webapp-go/internal/obutts"
|
||||||
"aleesa-webapp-go/internal/openweathermap"
|
"aleesa-webapp-go/internal/openweathermap"
|
||||||
"aleesa-webapp-go/internal/prazdnikisegodnyaru"
|
|
||||||
"aleesa-webapp-go/internal/randomfox"
|
"aleesa-webapp-go/internal/randomfox"
|
||||||
"aleesa-webapp-go/internal/thecatapi"
|
"aleesa-webapp-go/internal/thecatapi"
|
||||||
"aleesa-webapp-go/internal/xkcdru"
|
"aleesa-webapp-go/internal/xkcdru"
|
||||||
@@ -49,7 +50,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение
|
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение.
|
||||||
if j.Misc.Fwdcnt > cfg.ForwardsMax {
|
if j.Misc.Fwdcnt > cfg.ForwardsMax {
|
||||||
log.Warnf("Discarding msg with fwd_cnt exceeding max value: %s", msg)
|
log.Warnf("Discarding msg with fwd_cnt exceeding max value: %s", msg)
|
||||||
|
|
||||||
@@ -60,15 +61,15 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
|
|
||||||
sendTo = j.Plugin
|
sendTo = j.Plugin
|
||||||
|
|
||||||
// Классифицируем входящие сообщения. Первым делом, попробуем определить команды
|
// Классифицируем входящие сообщения. Первым делом, попробуем определить команды.
|
||||||
if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
|
if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
|
||||||
var cmd = j.Message[len(j.Misc.Csign):]
|
var cmd = j.Message[len(j.Misc.Csign):]
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case cmd == "cat" || cmd == "кис":
|
case cmd == "cat" || cmd == "кис":
|
||||||
// Пытаемся вычитать ответ thecatapi 3 раза
|
// Пытаемся вычитать ответ thecatapi 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = thecatapi.APIClient(cfg)
|
answer, err = thecatapi.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query api.thecatapi.com: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query api.thecatapi.com: %v", i+1, err)
|
||||||
@@ -83,18 +84,20 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
if answer == "" {
|
if answer == "" {
|
||||||
j.Message = "Нету кошечек, все разбежались."
|
j.Message = "Нету кошечек, все разбежались."
|
||||||
} else {
|
} else {
|
||||||
// for telegram return answer in markdown format
|
// for telegram return answer in markdown format.
|
||||||
if j.Plugin == "telegram" {
|
if j.Plugin == "telegram" {
|
||||||
cats := []string{"龴ↀ◡ↀ龴", "=^..^=", "≧◔◡◔≦", "^ↀᴥↀ^"}
|
cats := []string{"龴ↀ◡ↀ龴", "=^..^=", "≧◔◡◔≦", "^ↀᴥↀ^"} //nolint: gosmopolitan
|
||||||
|
|
||||||
j.Message = fmt.Sprintf("[%s](%s)", cats[rand.IntN(len(cats))], answer)
|
j.Message = fmt.Sprintf("[%s](%s)", cats[rand.IntN(len(cats))], answer)
|
||||||
} else {
|
} else {
|
||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "fox" || cmd == "лис":
|
case cmd == "fox" || cmd == "лис":
|
||||||
// Пытаемся вычитать ответ randonfox.ca 3 раза
|
// Пытаемся вычитать ответ randonfox.ca 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = randomfox.APIClient(cfg)
|
answer, err = randomfox.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query randomfox.ca: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query randomfox.ca: %v", i+1, err)
|
||||||
@@ -115,9 +118,10 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "xkcd":
|
case cmd == "xkcd":
|
||||||
// Пытаемся вычитать ответ xkcd.ru 3 раза
|
// Пытаемся вычитать ответ xkcd.ru 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = xkcdru.APIClient(cfg)
|
answer, err = xkcdru.APIClient(cfg)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -139,10 +143,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "buni":
|
case cmd == "buni":
|
||||||
// Пытаемся вычитать ответ www.bunicomic.com 3 раза
|
// Пытаемся вычитать ответ www.bunicomic.com 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = bunicomic.APIClient(cfg)
|
answer, err = bunicomic.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query www.bunicomic.com: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query www.bunicomic.com: %v", i+1, err)
|
||||||
@@ -163,10 +168,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "anek" || cmd == "анек" || cmd == "анекдот":
|
case cmd == "anek" || cmd == "анек" || cmd == "анекдот":
|
||||||
// Пытаемся вычитать ответ www.anekdot.ru 3 раза
|
// Пытаемся вычитать ответ www.anekdot.ru 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = anekdotru.APIClient(cfg)
|
answer, err = anekdotru.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query www.anekdot.ru: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query www.anekdot.ru: %v", i+1, err)
|
||||||
@@ -187,10 +193,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "monkeyuser":
|
case cmd == "monkeyuser":
|
||||||
// Пытаемся вычитать ответ www.bunicomic.com 3 раза
|
// Пытаемся вычитать ответ monkeyuser.com 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = monkeyuser.APIClient(cfg)
|
answer, err = monkeyuser.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query monkeyuser.com: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query monkeyuser.com: %v", i+1, err)
|
||||||
@@ -211,10 +218,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "butt" || cmd == "booty" || cmd == "ass" || cmd == "попа" || cmd == "попка":
|
case cmd == "butt" || cmd == "booty" || cmd == "ass" || cmd == "попа" || cmd == "попка":
|
||||||
// Пытаемся вычитать ответ api.obutts.ru 3 раза
|
// Пытаемся вычитать ответ api.obutts.ru 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = obutts.APIClient(cfg)
|
answer, err = obutts.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query api.obutts.ru: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query api.obutts.ru: %v", i+1, err)
|
||||||
@@ -231,15 +239,17 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
} else {
|
} else {
|
||||||
if j.Plugin == "telegram" {
|
if j.Plugin == "telegram" {
|
||||||
arts := []string{"(__(__)", "(_!_)", "(__.__)"}
|
arts := []string{"(__(__)", "(_!_)", "(__.__)"}
|
||||||
|
|
||||||
j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer)
|
j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer)
|
||||||
} else {
|
} else {
|
||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "titts" || cmd == "boobs" || cmd == "tities" || cmd == "boobies" || cmd == "сиси" || cmd == "сисечки":
|
case cmd == "titts" || cmd == "boobs" || cmd == "tities" || cmd == "boobies" || cmd == "сиси" || cmd == "сисечки":
|
||||||
// Пытаемся вычитать ответ api.oboobs.ru 3 раза
|
// Пытаемся вычитать ответ api.oboobs.ru 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = oboobs.APIClient(cfg)
|
answer, err = oboobs.APIClient(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query api.oboobs.ru: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query api.oboobs.ru: %v", i+1, err)
|
||||||
@@ -256,18 +266,20 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
} else {
|
} else {
|
||||||
if j.Plugin == "telegram" {
|
if j.Plugin == "telegram" {
|
||||||
arts := []string{"(. )( .)", "( . Y . )", "(o)(o)", "( @ )( @ )", "(.)(.)"}
|
arts := []string{"(. )( .)", "( . Y . )", "(o)(o)", "( @ )( @ )", "(.)(.)"}
|
||||||
|
|
||||||
j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer)
|
j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer)
|
||||||
} else {
|
} else {
|
||||||
j.Message = answer
|
j.Message = answer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "drink" || cmd == "праздник":
|
case cmd == "drink" || cmd == "праздник":
|
||||||
// Пытаемся вычитать ответ prazdniki-segodnya.ru 3 раза
|
// Пытаемся вычитать ответ celebratoday.com 3 раза.
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = prazdnikisegodnyaru.PsrClient(cfg)
|
answer, err = celebratoday.Client(cfg) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query prazdniki-segodnya.ru: %v", i+1, err)
|
log.Errorf("Try %d/3 unable to query celebratoday.com: %v", i+1, err)
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -283,8 +295,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "frog" || cmd == "лягушка":
|
case cmd == "frog" || cmd == "лягушка":
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = flickr.SearchByTags(cfg, []string{"frog", "toad", "amphibian"})
|
answer, err = flickr.SearchByTags(cfg, []string{"frog", "toad", "amphibian"}) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query flickr api about frogs: %s", i+1, err)
|
log.Errorf("Try %d/3 unable to query flickr api about frogs: %s", i+1, err)
|
||||||
@@ -302,6 +314,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
// Костыль для красивого ответа в telegram.
|
// Костыль для красивого ответа в telegram.
|
||||||
if j.Plugin == "telegram" {
|
if j.Plugin == "telegram" {
|
||||||
art := []string{"frog", "toad", "лягушка"}
|
art := []string{"frog", "toad", "лягушка"}
|
||||||
|
|
||||||
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
|
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,8 +322,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "owl" || cmd == "сова" || cmd == "сыч":
|
case cmd == "owl" || cmd == "сова" || cmd == "сыч":
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = flickr.SearchByTags(cfg, []string{"owlet", "owl", "raptor", "bird of prey", "nocturnal bird", "barn owl", "tawny owl", "brown owl"})
|
answer, err = flickr.SearchByTags(cfg, []string{"owlet", "owl", "raptor", "bird of prey", "nocturnal bird", "barn owl", "tawny owl", "brown owl"}) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query flickr api about owls: %s", i+1, err)
|
log.Errorf("Try %d/3 unable to query flickr api about owls: %s", i+1, err)
|
||||||
@@ -334,8 +347,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "horse" || cmd == "лошадь" || cmd == "лошадка":
|
case cmd == "horse" || cmd == "лошадь" || cmd == "лошадка":
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = flickr.SearchByTags(cfg, []string{"horse", "equine", "stallion", "mare", "steed", "thoroughbred"})
|
answer, err = flickr.SearchByTags(cfg, []string{"horse", "equine", "stallion", "mare", "steed", "thoroughbred"}) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query flickr api about horses: %s", i+1, err)
|
log.Errorf("Try %d/3 unable to query flickr api about horses: %s", i+1, err)
|
||||||
@@ -353,6 +366,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
// Костыль для красивого ответа в telegram.
|
// Костыль для красивого ответа в telegram.
|
||||||
if j.Plugin == "telegram" {
|
if j.Plugin == "telegram" {
|
||||||
art := []string{"horse", "лошадь", "лошадка"}
|
art := []string{"horse", "лошадь", "лошадка"}
|
||||||
|
|
||||||
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
|
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,8 +374,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "rabbit" || cmd == "bunny" || cmd == "кролик":
|
case cmd == "rabbit" || cmd == "bunny" || cmd == "кролик":
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = flickr.SearchByTags(cfg, []string{"bunny", "rabbit", "buck"})
|
answer, err = flickr.SearchByTags(cfg, []string{"bunny", "rabbit", "buck"}) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query flickr api about rabbits: %s", i+1, err)
|
log.Errorf("Try %d/3 unable to query flickr api about rabbits: %s", i+1, err)
|
||||||
@@ -385,8 +399,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
}
|
}
|
||||||
|
|
||||||
case cmd == "snail" || cmd == "улитка":
|
case cmd == "snail" || cmd == "улитка":
|
||||||
for i := 0; i < 3; i++ {
|
for i := range 3 {
|
||||||
answer, err = flickr.SearchByTags(cfg, []string{"snail", "slug"})
|
answer, err = flickr.SearchByTags(cfg, []string{"snail", "slug"}) //nolint: contextcheck // Fuck off.
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Try %d/3 unable to query flickr api about snails: %s", i+1, err)
|
log.Errorf("Try %d/3 unable to query flickr api about snails: %s", i+1, err)
|
||||||
@@ -404,6 +418,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
// Костыль для красивого ответа в telegram.
|
// Костыль для красивого ответа в telegram.
|
||||||
if j.Plugin == "telegram" {
|
if j.Plugin == "telegram" {
|
||||||
art := []string{"'-'_@_", "@╜", "@_'-'"}
|
art := []string{"'-'_@_", "@╜", "@_'-'"}
|
||||||
|
|
||||||
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
|
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +434,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
} else {
|
} else {
|
||||||
if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil {
|
if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil {
|
||||||
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
|
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
|
||||||
|
|
||||||
j.Message = "Я не знаю, какая погода в " + city
|
j.Message = "Я не знаю, какая погода в " + city
|
||||||
} else {
|
} else {
|
||||||
j.Message = answer
|
j.Message = answer
|
||||||
@@ -433,6 +449,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
if regexp.MustCompile(`5$`).MatchString(cmdStr) {
|
if regexp.MustCompile(`5$`).MatchString(cmdStr) {
|
||||||
if answer, err := openweathermap.OwmClient(cfg, city, 5); err != nil {
|
if answer, err := openweathermap.OwmClient(cfg, city, 5); err != nil {
|
||||||
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
|
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
|
||||||
|
|
||||||
j.Message = "Я не знаю, какая погода в " + city
|
j.Message = "Я не знаю, какая погода в " + city
|
||||||
} else {
|
} else {
|
||||||
j.Message = answer
|
j.Message = answer
|
||||||
@@ -440,6 +457,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
} else {
|
} else {
|
||||||
if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil {
|
if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil {
|
||||||
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
|
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
|
||||||
|
|
||||||
j.Message = "Я не знаю, какая погода в " + city
|
j.Message = "Я не знаю, какая погода в " + city
|
||||||
} else {
|
} else {
|
||||||
j.Message = answer
|
j.Message = answer
|
||||||
@@ -467,7 +485,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Настало время формировать json и засылать его в дальше
|
// Настало время формировать json и засылать его в дальше.
|
||||||
var message SMsg
|
var message SMsg
|
||||||
|
|
||||||
message.From = j.From
|
message.From = j.From
|
||||||
@@ -493,7 +511,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Заталкиваем наш json в редиску
|
// Заталкиваем наш json в редиску.
|
||||||
if err := cfg.RedisClient.Publish(ctx, sendTo, data).Err(); err != nil {
|
if err := cfg.RedisClient.Publish(ctx, sendTo, data).Err(); err != nil {
|
||||||
log.Warnf("Unable to send data to redis channel %s: %s", sendTo, err)
|
log.Warnf("Unable to send data to redis channel %s: %s", sendTo, err)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ func ReadConfig() error {
|
|||||||
for _, location := range locations {
|
for _, location := range locations {
|
||||||
fileInfo, err := os.Stat(location)
|
fileInfo, err := os.Stat(location)
|
||||||
|
|
||||||
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
|
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
|
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
|
||||||
if fileInfo.Size() > ConfigFileSize {
|
if fileInfo.Size() > ConfigFileSize {
|
||||||
log.Warnf("Config file %s is too long for config, skipping", location)
|
log.Warnf("Config file %s is too long for config, skipping", location)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ func ReadConfig() error {
|
|||||||
|
|
||||||
buf, err := os.ReadFile(location)
|
buf, err := os.ReadFile(location)
|
||||||
|
|
||||||
// Не удалось прочитать, попробуем следующего кандидата
|
// Не удалось прочитать, попробуем следующего кандидата.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Skip reading config file %s: %s", location, err)
|
log.Warnf("Skip reading config file %s: %s", location, err)
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ func ReadConfig() error {
|
|||||||
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
|
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
|
||||||
var (
|
var (
|
||||||
sampleConfig *config.MyConfig
|
sampleConfig *config.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 {
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ func ReadConfig() error {
|
|||||||
|
|
||||||
tmpjson, err := json.Marshal(tmp)
|
tmpjson, err := json.Marshal(tmp)
|
||||||
|
|
||||||
// Не удалось преобразовать map-ку в json
|
// Не удалось преобразовать map-ку в json.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Skip parsing config file %s: %s", location, err)
|
log.Warnf("Skip parsing config file %s: %s", location, err)
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ func ReadConfig() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Валидируем значения из конфига
|
// Валидируем значения из конфига.
|
||||||
if sampleConfig.Server == "" {
|
if sampleConfig.Server == "" {
|
||||||
sampleConfig.Server = Host
|
sampleConfig.Server = Host
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func ReadConfig() error {
|
|||||||
sampleConfig.Loglevel = Loglevel
|
sampleConfig.Loglevel = Loglevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// sampleConfig.Log = "" if not set
|
// sampleConfig.Log = "" if not set.
|
||||||
|
|
||||||
if sampleConfig.Channel == "" {
|
if sampleConfig.Channel == "" {
|
||||||
log.Errorf("Channel field in config file %s must be set", location)
|
log.Errorf("Channel field in config file %s must be set", location)
|
||||||
@@ -129,7 +129,7 @@ func ReadConfig() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// sampleConfig.OpenWeatherMap.Country is false by default
|
// sampleConfig.OpenWeatherMap.Country is false by default.
|
||||||
|
|
||||||
// Теперь надо бы попробовать загрузить список User Agent-ов, с которыми ходить в разные апишки.
|
// Теперь надо бы попробовать загрузить список User Agent-ов, с которыми ходить в разные апишки.
|
||||||
uaFile := sampleConfig.DataDir + "/useragents.txt"
|
uaFile := sampleConfig.DataDir + "/useragents.txt"
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ func SigHandler(cfg *config.MyConfig) {
|
|||||||
case syscall.SIGQUIT:
|
case syscall.SIGQUIT:
|
||||||
log.Info("Got SIGQUIT, quitting")
|
log.Info("Got SIGQUIT, quitting")
|
||||||
|
|
||||||
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
|
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,21 +45,21 @@ func ValidateRmsg(cfg *config.MyConfig, j RMsg, msg string) (RMsg, error) {
|
|||||||
return j, emsg
|
return j, emsg
|
||||||
}
|
}
|
||||||
|
|
||||||
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
|
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет.
|
||||||
// j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка
|
// j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка.
|
||||||
// j.Misc.Csign если нам его не передали, возьмём значение из конфига
|
// j.Misc.Csign если нам его не передали, возьмём значение из конфига.
|
||||||
if exist := j.Misc.Csign; exist == "" {
|
if exist := j.Misc.Csign; exist == "" {
|
||||||
j.Misc.Csign = cfg.Csign
|
j.Misc.Csign = cfg.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 можно не передавать, тогда будет пустая строка.
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ var (
|
|||||||
// Config структурка, содержащая данные конфига.
|
// Config структурка, содержащая данные конфига.
|
||||||
Config *config.MyConfig
|
Config *config.MyConfig
|
||||||
|
|
||||||
// ctx контекст редиски.
|
// Ctx контекст редиски.
|
||||||
Ctx = context.Background()
|
Ctx = context.Background()
|
||||||
|
|
||||||
// Subscriber объектик сабскрайбера редиски.
|
// Subscriber объектик сабскрайбера редиски.
|
||||||
|
|||||||
@@ -1,43 +1,45 @@
|
|||||||
package webapp
|
package webapp
|
||||||
|
|
||||||
// RMsg структурка, описывающая входящее сообщение из pubsub-канала redis-ки.
|
type (
|
||||||
type RMsg struct {
|
// RMsg структурка, описывающая входящее сообщение из pubsub-канала redis-ки.
|
||||||
From string `json:"from,omitempty"`
|
RMsg struct {
|
||||||
Chatid string `json:"chatid,omitempty"`
|
From string `json:"from,omitempty"`
|
||||||
Userid string `json:"userid,omitempty"`
|
Chatid string `json:"chatid,omitempty"`
|
||||||
Threadid string `json:"threadid,omitempty"`
|
Userid string `json:"userid,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Threadid string `json:"threadid,omitempty"`
|
||||||
Plugin string `json:"plugin,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Plugin string `json:"plugin,omitempty"`
|
||||||
Misc struct {
|
Mode string `json:"mode,omitempty"`
|
||||||
Answer int64 `json:"answer,omitempty"`
|
Misc struct { //nolint: govet // Мне так удобнее.
|
||||||
Botnick string `json:"bot_nick,omitempty"`
|
Answer int64 `json:"answer,omitempty"`
|
||||||
Csign string `json:"csign,omitempty"`
|
Botnick string `json:"bot_nick,omitempty"`
|
||||||
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
Csign string `json:"csign,omitempty"`
|
||||||
GoodMorning int64 `json:"good_morning,omitempty"`
|
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
|
||||||
Msgformat int64 `json:"msg_format,omitempty"`
|
GoodMorning int64 `json:"good_morning,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Msgformat int64 `json:"msg_format,omitempty"`
|
||||||
} `json:"Misc"`
|
Username string `json:"username,omitempty"`
|
||||||
}
|
} `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"`
|
||||||
Threadid string `json:"threadid"`
|
Threadid string `json:"threadid"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Plugin string `json:"plugin"`
|
Plugin string `json:"plugin"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
Misc struct {
|
Misc struct { //nolint: govet // Мне так удобнее.
|
||||||
Answer int64 `json:"answer"`
|
Answer int64 `json:"answer"`
|
||||||
Botnick string `json:"bot_nick"`
|
Botnick string `json:"bot_nick"`
|
||||||
Csign string `json:"csign"`
|
Csign string `json:"csign"`
|
||||||
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"`
|
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: */
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package xkcdru provides api to xkcd.ru.
|
||||||
package xkcdru
|
package xkcdru
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -25,7 +26,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil) //nolint: noctx
|
req, err := http.NewRequest(http.MethodGet, url, http.NoBody) //nolint: noctx
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -48,7 +49,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
|
|||||||
}
|
}
|
||||||
}(resp.Body)
|
}(resp.Body)
|
||||||
|
|
||||||
// Response body нам не интересен, нам интересен статус и нам интересен заголовок Location
|
// Response body нам не интересен, нам интересен статус и нам интересен заголовок Location.
|
||||||
if resp.StatusCode == 302 {
|
if resp.StatusCode == 302 {
|
||||||
location := resp.Header.Get("Location")
|
location := resp.Header.Get("Location")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user