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:
eleksir
2026-02-10 20:05:13 +03:00
parent ff57b0af7d
commit f2fc168abe
28 changed files with 1298 additions and 268 deletions

View File

@@ -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:]'

View File

@@ -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**. Это и есть наше приложение.
### Как это запустить? ### Как это запустить?

View File

@@ -1,3 +1,4 @@
// Implements aleesa-web-go application.
package main package main
import ( import (

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

View File

@@ -1,3 +1,4 @@
// Implements initialisation app for flickr service. Gains Tokens, etc.
package main package main
import ( import (

View File

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

View File

@@ -1,3 +1,4 @@
// Package main ensures that oauth keys downloaded correctly and client part is working.
package main package main
import ( import (

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ var (
// Config структурка, содержащая данные конфига. // Config структурка, содержащая данные конфига.
Config *config.MyConfig Config *config.MyConfig
// ctx контекст редиски. // Ctx контекст редиски.
Ctx = context.Background() Ctx = context.Background()
// Subscriber объектик сабскрайбера редиски. // Subscriber объектик сабскрайбера редиски.

View File

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

View File

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