From f2fc168abe41d465395ccca9a180b27367e60b75 Mon Sep 17 00:00:00 2001 From: eleksir Date: Tue, 10 Feb 2026 20:05:13 +0300 Subject: [PATCH] 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 --- .golangci.yml | 715 +++++++++++++++++- README.md | 4 +- cmd/aleesa-webapp-go/main.go | 1 + cmd/celebratoday_test/main.go | 44 ++ cmd/flickr_init/main.go | 1 + cmd/flickr_populate/main.go | 1 + cmd/flickr_test/main.go | 1 + internal/anekdotru/anekdotru.go | 11 +- internal/bunicomic/bunicomic.go | 5 +- internal/celebratoday/celebratoday.go | 223 ++++++ internal/config/types.go | 58 +- internal/flickr/flickr.go | 71 +- internal/log/log.go | 13 +- internal/monkeyuser/monkeyuser.go | 21 +- internal/oboobs/oboobs.go | 19 +- internal/obutts/obutts.go | 19 +- internal/openweathermap/openweathermap.go | 41 +- internal/pcachedb/pcachedb.go | 15 +- .../prazdnikisegodnyaru.go | 36 +- internal/randomfox/randomfox.go | 13 +- internal/thecatapi/thecatapi.go | 27 +- internal/webapp/MsgParser.go | 106 +-- internal/webapp/ReadConfig.go | 18 +- internal/webapp/SigHandler.go | 4 +- internal/webapp/ValidateRmsg.go | 14 +- internal/webapp/globals.go | 2 +- internal/webapp/types.go | 78 +- internal/xkcdru/xkcdru.go | 5 +- 28 files changed, 1298 insertions(+), 268 deletions(-) create mode 100644 cmd/celebratoday_test/main.go create mode 100644 internal/celebratoday/celebratoday.go diff --git a/.golangci.yml b/.golangci.yml index b1f27e6..dcf06fd 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,9 +1,14 @@ version: "2" run: - go: "1.22" + go: "1.25" modules-download-mode: readonly tests: false 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: default: none enable: @@ -11,27 +16,33 @@ linters: - asciicheck - bodyclose - containedctx - - copyloopvar + - contextcheck - decorder - durationcheck - errcheck - errchkjson - errorlint - exhaustive + - exptostd - ginkgolinter - gocheckcompilerdirectives - gocritic + - godoclint - godot - - goheader + - godox + - goprintffuncname - gosec + - gosmopolitan - govet - grouper - - iface - importas - - inamedparam - ineffassign + - intrange - ireturn - makezero + - mirror + - misspell + - modernize - nakedret - nilerr - nilnil @@ -45,19 +56,690 @@ linters: - reassign - revive - rowserrcheck - - sloglint - staticcheck - unconvert - - unparam - unused + - usestdlibvars - wastedassign - whitespace - wsl_v5 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: - - 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: generated: lax presets: @@ -79,3 +761,16 @@ formatters: - third_party$ - builtin$ - 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:]' diff --git a/README.md b/README.md index 3df94d4..77d9c06 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Как это собрать? -Понадобится компилятор go версии 1.25.1 или более новый. +Понадобится компилятор go версии 1.25.3 или более новый. Сам процесс сборки простой, достаточно запустить make или gmake, в зависимости от платформы: @@ -24,7 +24,7 @@ make ``` -и через некоторое время мы получаем бинарник **aleesa-irc-go**. Это и есть наше приложение. +и через некоторое время мы получаем бинарник **aleesa-webapp-go**. Это и есть наше приложение. ### Как это запустить? diff --git a/cmd/aleesa-webapp-go/main.go b/cmd/aleesa-webapp-go/main.go index 17d3e9d..d1c48d0 100644 --- a/cmd/aleesa-webapp-go/main.go +++ b/cmd/aleesa-webapp-go/main.go @@ -1,3 +1,4 @@ +// Implements aleesa-web-go application. package main import ( diff --git a/cmd/celebratoday_test/main.go b/cmd/celebratoday_test/main.go new file mode 100644 index 0000000..d88b7ea --- /dev/null +++ b/cmd/celebratoday_test/main.go @@ -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) +} diff --git a/cmd/flickr_init/main.go b/cmd/flickr_init/main.go index bce9661..cf3560d 100644 --- a/cmd/flickr_init/main.go +++ b/cmd/flickr_init/main.go @@ -1,3 +1,4 @@ +// Implements initialisation app for flickr service. Gains Tokens, etc. package main import ( diff --git a/cmd/flickr_populate/main.go b/cmd/flickr_populate/main.go index e847689..dda43bd 100644 --- a/cmd/flickr_populate/main.go +++ b/cmd/flickr_populate/main.go @@ -1,3 +1,4 @@ +// Package main get flickr oauth keys required for proper client work an stores it locally. package main import ( diff --git a/cmd/flickr_test/main.go b/cmd/flickr_test/main.go index 5f5de87..d713f2d 100644 --- a/cmd/flickr_test/main.go +++ b/cmd/flickr_test/main.go @@ -1,3 +1,4 @@ +// Package main ensures that oauth keys downloaded correctly and client part is working. package main import ( diff --git a/internal/anekdotru/anekdotru.go b/internal/anekdotru/anekdotru.go index 4089507..cf3cc97 100644 --- a/internal/anekdotru/anekdotru.go +++ b/internal/anekdotru/anekdotru.go @@ -1,3 +1,4 @@ +// Package anekdotru implemets client for anekdot.ru service. package anekdotru import ( @@ -23,11 +24,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { url = "https://www.anekdot.ru/rss/randomu.html" ) - err := requests. - URL(url). - UserAgent(userAgent). - ToBytesBuffer(&respBody). - Fetch(ctx) + err := requests.URL(url).UserAgent(userAgent).ToBytesBuffer(&respBody).Fetch(ctx) if err != nil { 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] != "") { re = regexp.MustCompile(`'\);`) + a := re.Split(aneks[1], 2) if (len(a) >= 2) && (a[0] != "") { @@ -51,7 +49,9 @@ func APIClient(cfg *config.MyConfig) (string, error) { anek = re.ReplaceAll(anek, []byte(`"`)) re = regexp.MustCompile(`\\"`) + anekJSON := re.ReplaceAll(anek, []byte(`"`)) + anekJSON = bytes.ToValidUTF8(anekJSON, []byte{0xef, 0xbf, 0xbd}) var anekMap []string @@ -60,6 +60,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { } re = regexp.MustCompile(`
`) + answer := re.ReplaceAll([]byte(anekMap[0]), []byte("\n")) return string(answer), nil diff --git a/internal/bunicomic/bunicomic.go b/internal/bunicomic/bunicomic.go index 128b904..07e27fc 100644 --- a/internal/bunicomic/bunicomic.go +++ b/internal/bunicomic/bunicomic.go @@ -1,3 +1,4 @@ +// Package bunicomic implements client for bunicomic.com service. package bunicomic import ( @@ -48,13 +49,13 @@ func APIClient(cfg *config.MyConfig) (string, error) { if n.Type == html.ElementNode && n.Data == "meta" { for _, meta := range n.Attr { 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) } } } - // 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 { meta(c) } diff --git a/internal/celebratoday/celebratoday.go b/internal/celebratoday/celebratoday.go new file mode 100644 index 0000000..b22070d --- /dev/null +++ b/internal/celebratoday/celebratoday.go @@ -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: */ diff --git a/internal/config/types.go b/internal/config/types.go index ce475b2..1687709 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -1,4 +1,4 @@ -// Этот package нужен, чтобы разбить кольцевую зависимость. +// Package config нужен, чтобы разбить кольцевую зависимость. package config import ( @@ -6,32 +6,34 @@ import ( "github.com/go-redis/redis/v8" ) -// MyConfig структурка, описывающая конфиг. -type MyConfig struct { - Server string `json:"server,omitempty"` - Port int `json:"port,omitempty"` - Timeout int `json:"timeout,omitempty"` - Loglevel string `json:"loglevel,omitempty"` - Log string `json:"log,omitempty"` - Channel string `json:"channel,omitempty"` - DataDir string `json:"datadir,omitempty"` - Csign string `json:"csign,omitempty"` - ForwardsMax int64 `json:"forwards_max,omitempty"` - OpenWeatherMap struct { - Enabled bool `json:"enabled,omitempty"` - Country bool `json:"country,omitempty"` - Appid string `json:"appid,omitempty"` - } `json:"openweathermap,omitempty"` - Flickr struct { - Enabled bool `json:"enabled,omitempty"` - Key string `json:"key,omitempty"` - Secret string `json:"secret,omitempty"` - OAuthToken string `json:"oauth_token,omitempty"` - OAuthTokenSecret string `json:"oauth_token_secret,omitempty"` - } `json:"flickr,omitempty"` - UserAgents []string `json:"user_agents,omitempty"` - PcacheDB map[string]*pebble.DB - RedisClient *redis.Client -} +type ( + // MyConfig структурка, описывающая конфиг. + MyConfig struct { //nolint: govet // Идите в пень, мне так удобнее. + Server string `json:"server,omitempty"` + Port int `json:"port,omitempty"` + Timeout int `json:"timeout,omitempty"` + Loglevel string `json:"loglevel,omitempty"` + Log string `json:"log,omitempty"` + Channel string `json:"channel,omitempty"` + DataDir string `json:"datadir,omitempty"` + Csign string `json:"csign,omitempty"` + ForwardsMax int64 `json:"forwards_max,omitempty"` + OpenWeatherMap struct { //nolint: govet // Идите в пень, мне так удобнее. + Enabled bool `json:"enabled,omitempty"` + Country bool `json:"country,omitempty"` + Appid string `json:"appid,omitempty"` + } `json:"openweathermap"` + Flickr struct { //nolint: govet // Идите в пень, мне так удобнее. + Enabled bool `json:"enabled,omitempty"` + Key string `json:"key,omitempty"` + Secret string `json:"secret,omitempty"` + OAuthToken string `json:"oauth_token,omitempty"` + OAuthTokenSecret string `json:"oauth_token_secret,omitempty"` + } `json:"flickr"` + UserAgents []string `json:"user_agents,omitempty"` + PcacheDB map[string]*pebble.DB + RedisClient *redis.Client + } +) /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/flickr/flickr.go b/internal/flickr/flickr.go index bcf47a4..d1fe732 100644 --- a/internal/flickr/flickr.go +++ b/internal/flickr/flickr.go @@ -1,3 +1,4 @@ +// Package flickr implements flickr.com service client. package flickr import ( @@ -17,27 +18,29 @@ import ( "gopkg.in/masci/flickr.v3/test" ) -// SearchResult структура, описывающая ответ api на поисковой запрос. -type SearchResult struct { - Photos struct { - Page int64 `json:"page,omitempty"` - Pages int64 `json:"pages,omitempty"` - Perpage int64 `json:"perpage,omitempty"` - Total int64 `json:"total,omitempty"` - Photo []struct { - Farm int64 `json:"farm,omitempty"` - ID string `json:"id,omitempty"` - Isfamily int64 `json:"isfamily,omitempty"` - Isfriend int64 `json:"isfriend,omitempty"` - Ispublic int64 `json:"ispublic,omitempty"` - Owner string `json:"owner,omitempty"` - Secret string `json:"secret,omitempty"` - Server string `json:"server,omitempty"` - Title string `json:"title,omitempty"` - } `json:"photo,omitempty"` - } `json:"photos,omitempty"` - Stat string `json:"stat,omitempty"` -} +type ( + // SearchResult структура, описывающая ответ api на поисковой запрос. + SearchResult struct { //nolint: govet // Идите в пень, мне так удобнее. + Photos struct { //nolint: govet // Идите в пень, мне так удобнее. + Page int64 `json:"page,omitempty"` + Pages int64 `json:"pages,omitempty"` + Perpage int64 `json:"perpage,omitempty"` + Total int64 `json:"total,omitempty"` + Photo []struct { //nolint: govet // Идите в пень, мне так удобнее. + Farm int64 `json:"farm,omitempty"` + ID string `json:"id,omitempty"` + Isfamily int64 `json:"isfamily,omitempty"` + Isfriend int64 `json:"isfriend,omitempty"` + Ispublic int64 `json:"ispublic,omitempty"` + Owner string `json:"owner,omitempty"` + Secret string `json:"secret,omitempty"` + Server string `json:"server,omitempty"` + Title string `json:"title,omitempty"` + } `json:"photo"` + } `json:"photos"` + Stat string `json:"stat,omitempty"` + } +) // APIClientInit генерит и сохраняет token и token secret с помощью даденных пользователем key и secret. // В процессе выдаём юзеру в консоль урл и запрос открыть его в браузере, чтобы получить verifier key, который нам @@ -47,15 +50,15 @@ func APIClientInit(cfg *config.MyConfig) error { client := flickr.NewFlickrClient(cfg.Flickr.Key, cfg.Flickr.Secret) - // Первым делом достанем request token + // Первым делом достанем request token. tok, err := flickr.GetRequestToken(client) if err != nil { return fmt.Errorf("unable to get Request Token: %w", err) } - // Соорудим авторизационный URL - url, err := flickr.GetAuthorizeUrl(client, tok) + // Соорудим авторизационный URL. + link, err := flickr.GetAuthorizeUrl(client, tok) if err != nil { return fmt.Errorf("unable to get auth URL: %w", err) @@ -63,11 +66,12 @@ func APIClientInit(cfg *config.MyConfig) error { // А теперь ручные действия пользователя. // Выдаём ему url, который надо ввести в адресную строку браузера, в браузере сгенерится verifier code, который нам - // нужен, чтобы вынуть из flicker api oauth token, secret, access token + // нужен, чтобы вынуть из flicker api oauth token, secret, access token. 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.Scanln(&oauthVerifier) // Наконец, достаём 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.OAuthTokenSecret = oauthSecret @@ -152,12 +157,12 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { // разметки, а во-вторых, этот самый flickr.DoGet() валится с segfault-ом, просто потому что. Схема не такая, на // которую он рассчитан. Но нам повезло, что можно просто вытащить подписанный url и заправить его в свой // http-клиент. - url := c.GetUrl() + link := c.GetUrl() var response SearchResult err := requests. - URL(url). + URL(link). UserAgent(userAgent). ToJSON(&response). Fetch(ctx) @@ -179,10 +184,10 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { // Подписываем запрос oauth-говном. c.OAuthSign() - url = c.GetUrl() + link = c.GetUrl() err = requests. - URL(url). + URL(link). UserAgent(userAgent). ToJSON(&response). Fetch(ctx) @@ -200,10 +205,10 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { var u SearchResult - for i := 0; i < len(response.Photos.Photo); i++ { + for i := range len(response.Photos.Photo) { 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 { skip = true diff --git a/internal/log/log.go b/internal/log/log.go index 0c77912..374a638 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,3 +1,4 @@ +// Package log implements logging facility as golang slog module wrapper. package log import ( @@ -33,9 +34,9 @@ func Init(level string, fileDescriptor *os.File) { opts := &slog.HandlerOptions{ // 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 - // check that we are handling the time key + // Check that we are handling the time key. if a.Key != slog.TimeKey { 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) { slog.Error(message) } @@ -72,7 +73,7 @@ func Errorf(format string, a ...any) { slog.Error(fmt.Sprintf(format, a...)) } -// Warn passes message directly to slog.Warn(). +// Warn passes message directly to [slog.Warn()]. func Warn(message string) { slog.Warn(message) } @@ -82,7 +83,7 @@ func Warnf(format string, a ...any) { slog.Warn(fmt.Sprintf(format, a...)) } -// Info passes message directly to slog.Info(). +// Info passes message directly to [slog.Info()]. func Info(message string) { slog.Info(message) } @@ -92,7 +93,7 @@ func Infof(format string, a ...any) { slog.Info(fmt.Sprintf(format, a...)) } -// Debug passes message directly to slog.Debug(). +// Debug passes message directly to [slog.Debug()]. func Debug(message string) { slog.Debug(message) } diff --git a/internal/monkeyuser/monkeyuser.go b/internal/monkeyuser/monkeyuser.go index 68b0c36..9ae0372 100644 --- a/internal/monkeyuser/monkeyuser.go +++ b/internal/monkeyuser/monkeyuser.go @@ -1,3 +1,4 @@ +// Package monkeyuser implements client for service www.monkeyusers.com. package monkeyuser import ( @@ -14,11 +15,13 @@ import ( "golang.org/x/net/html" ) -// MonkeyUsers описывает структуру json-массива ответа www.monkeyusers.com. -type MonkeyUsers []struct { - Title string `json:"title"` - URL string `json:"url"` -} +type ( + // MonkeyUsers описывает структуру json-массива ответа www.monkeyusers.com. + MonkeyUsers []struct { + Title string `json:"title"` + URL string `json:"url"` + } +) // APIClient клиент сервиса monkeyuser.com. 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) } - // Парсим body ответа от сервера + // Парсим body ответа от сервера. doc, err := html.Parse(strings.NewReader(body)) if err != nil { @@ -91,7 +94,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { if n.Type == html.ElementNode && n.Data == "img" { for _, a_href := range n.Attr { //nolint: revive // Если у нас class == "lazyload small-image", то после него ключ data-src должен содержать - // искомый relative url + // искомый relative url. if a_href.Key == "class" && a_href.Val == "logo" { 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 { link(c) } @@ -113,7 +116,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { link(doc) - // Возвращаем ссылку на картинку buni comic + // Возвращаем ссылку на картинку buni comic. if hyperlink != "" { return fmt.Sprintf("%s%s", baseURL, hyperlink), nil } diff --git a/internal/oboobs/oboobs.go b/internal/oboobs/oboobs.go index 632a504..0ab0e51 100644 --- a/internal/oboobs/oboobs.go +++ b/internal/oboobs/oboobs.go @@ -1,3 +1,4 @@ +// Package oboobs implements api.oboobs.ru service client. package oboobs import ( @@ -9,14 +10,16 @@ import ( "github.com/carlmjohnson/requests" ) -// Oboobs структура, возвращаемая на запрос в oboobs.ru. -type Oboobs struct { - ID int `json:"id,omitempty"` - Author string `json:"author,omitempty"` - Rank int `json:"rank,omitempty"` - Model string `json:"model,omitempty"` - Preview string `json:"preview"` -} +type ( + // Oboobs структура, возвращаемая на запрос в oboobs.ru. + Oboobs struct { //nolint: govet // Идите в пень, мне так удобнее. + ID int `json:"id,omitempty"` + Author string `json:"author,omitempty"` + Rank int `json:"rank,omitempty"` + Model string `json:"model,omitempty"` + Preview string `json:"preview"` + } +) // APIClient клиент сервиса oboobs.ru. func APIClient(cfg *config.MyConfig) (string, error) { diff --git a/internal/obutts/obutts.go b/internal/obutts/obutts.go index f210b58..d2456d5 100644 --- a/internal/obutts/obutts.go +++ b/internal/obutts/obutts.go @@ -1,3 +1,4 @@ +// Package obutts implements api.obutts.ru service client. package obutts import ( @@ -9,14 +10,16 @@ import ( "github.com/carlmjohnson/requests" ) -// Obutts структура, возвращаемая на запрос в obutts.ru. -type Obutts struct { - ID int `json:"id,omitempty"` - Author string `json:"author,omitempty"` - Rank int `json:"rank,omitempty"` - Model string `json:"model,omitempty"` - Preview string `json:"preview"` -} +type ( + // Obutts структура, возвращаемая на запрос в obutts.ru. + Obutts struct { //nolint: govet // Идите в пень, мне так удобнее. + ID int `json:"id,omitempty"` + Author string `json:"author,omitempty"` + Rank int `json:"rank,omitempty"` + Model string `json:"model,omitempty"` + Preview string `json:"preview"` + } +) // APIClient клиент сервиса obutts.ru. func APIClient(cfg *config.MyConfig) (string, error) { diff --git a/internal/openweathermap/openweathermap.go b/internal/openweathermap/openweathermap.go index cafd63d..f92deb8 100644 --- a/internal/openweathermap/openweathermap.go +++ b/internal/openweathermap/openweathermap.go @@ -1,3 +1,4 @@ +// Package openweathermap implements openweatermap.org service client. package openweathermap import ( @@ -14,14 +15,16 @@ import ( owm "github.com/briandowns/openweathermap" ) -// Структурка для передачи в обрабтчик запросов в openweathermap api. -type OwmItem struct { - City string - FakeName string - Where string - Lon float64 - Lat float64 -} +type ( + // OwmItem структурка для передачи в обрабтчик запросов в openweathermap api. + OwmItem struct { + City string + FakeName string + Where string + Lon float64 + Lat float64 + } +) // OwmClient обёртка с кэшированием для openweatermap.org. 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) + firstLetter := strings.SplitN(city, "", 2)[0] + city = fmt.Sprintf("%s%s", strings.ToUpper(firstLetter), city[1:]) city = strings.ToValidUTF8(city, "") switch city { case "Msk", "Default", "Dc", "Мск", "Dc-Universe": item.City = "Москва" + case "Spb", "Спб": item.City = "Санкт-Петербург" + case "Ект", "Ёбург", "Ебург", "Екат", "Ekt", "Eburg", "Ekat": item.City = "Екатеринбург" + case "Кудыкина гора": // Координаты Килиманжаро (-3.066524619045895, 37.35588473583672). item.Lat = -3.066524619045895 item.Lon = 37.35588473583672 item.FakeName = "Килиманжаро" item.Where = "на горе" + default: item.City = city item.Where = "в городе" @@ -96,8 +105,8 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) { } if item.City != "" { - if err = w.CurrentByName(item.City); err != nil { - // TODO: extend error + if err := w.CurrentByName(item.City); err != nil { + // TODO: extend error. return answer, err } @@ -105,8 +114,8 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) { } else { coord := owm.Coordinates{Longitude: item.Lon, Latitude: item.Lat} - if err = w.CurrentByCoordinates(&coord); err != nil { - // TODO: extend error + if err := w.CurrentByCoordinates(&coord); err != nil { + // TODO: extend error. return answer, err } @@ -291,6 +300,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) { // Первое, что приходит в голову - коллизия хэшей sha256. if err != nil { log.Warnf("OWM-Cache: unable to parse string as int64 (sha256 collision?): %s", err) + answer, err := OwmAPIClient(cfg, item) // Бинго! у нас ещё и ошибка при обращении к api openwethermap, здесь мы ничего сделать не можем. @@ -306,7 +316,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) { return answer, err } - // Кэш просрочен + // Кэш просрочен. if tsCache < tsNow { log.Debugf("OWM cache miss for city: %s", key) @@ -326,6 +336,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) { } key += "+value" + answer = pcachedb.GetValue(cfg, "cache", key) // Какие-то проблемы с кэшом - ts есть, но самого кэша нету. Более подробная инфа по идее уже попала в лог. @@ -334,7 +345,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) { if len(answer) < 140 { 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) } @@ -373,7 +384,7 @@ func UpdateOwmUserCache(cfg *config.MyConfig, chatid string, entry string, value return err } -// Возвращает из кэша город, которым интересовался пользователь из даденного чятика. +// QueryOwmUserCache возвращает из кэша город, которым интересовался пользователь из даденного чятика. func QueryOwmUserCache(cfg *config.MyConfig, chatid string, entry string) string { // Chatid как есть лучше на диск не пробовать класть, мало ли что туда юзер впишет. Возьмём от него sha256. var ( diff --git a/internal/pcachedb/pcachedb.go b/internal/pcachedb/pcachedb.go index dd5dc42..8319d18 100644 --- a/internal/pcachedb/pcachedb.go +++ b/internal/pcachedb/pcachedb.go @@ -1,3 +1,4 @@ +// Package pcachedb implements persistent cache using PebbleDB key-value library. package pcachedb import ( @@ -51,12 +52,12 @@ func GetValue(cfg *config.MyConfig, cacheID string, setting string) string { cacheHash := sha256.Sum256([]byte(cacheID)) database := fmt.Sprintf("cache_db/%x", cacheHash) - // Если БД не открыта, откроем её + // Если БД не открыта, откроем её. if _, ok := cfg.PcacheDB[database]; !ok { var options pebble.Options // По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными // настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук, - // что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений + // что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений. options.L0CompactionFileThreshold = 8 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 } -// saveKeyWithValue сохраним данные в БД. +// SaveKeyWithValue сохраним данные в БД. func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, value string) error { var ( cacheHash = sha256.Sum256([]byte(cacheID)) @@ -96,7 +97,7 @@ func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, valu err error ) - // Если БД не открыта, откроем её + // Если БД не открыта, откроем её. if _, ok := cfg.PcacheDB[database]; !ok { 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 { 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 diff --git a/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go b/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go index 8152075..4ac3a6d 100644 --- a/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go +++ b/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go @@ -1,3 +1,5 @@ +// Package prazdnikisegodnyaru implements prazdniki-segodnya.ru service client. +// Now defunct on Russian Federation area for some legal reasons. package prazdnikisegodnyaru import ( @@ -20,14 +22,14 @@ import ( func PsrClient(cfg *config.MyConfig) (string, error) { tsNow := time.Now() - // Вычислим expiration time для MSK+0 в формате unix timestamp + // Вычислим expiration time для MSK+0 в формате unix timestamp. loc, _ := time.LoadLocation("Europe/Moscow") year, month, day := tsNow.In(loc).Date() eTime := time.Date(year, month, day, 0, 0, 0, 0, loc).AddDate(0, 0, +1).Unix() tsStr := pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_timestamp") - // Кэш пустой, запросов пока не было + // Кэш пустой, запросов пока не было. if tsStr == "" { log.Debugf("PSR no cache entry for prazdnikisegodhyaru") @@ -49,14 +51,15 @@ func PsrClient(cfg *config.MyConfig) (string, error) { if err != nil { log.Debug("PSR cache miss") log.Warnf("PSR-Cache: unable to parse string as int64 (sha256 collision?): %s", err) + answer, err := PsrAPIClient(cfg) - // Бинго! у нас ещё и ошибка при обращении к "api" prazdnikisegodhyaru, здесь мы ничего сделать не можем + // Бинго! у нас ещё и ошибка при обращении к "api" prazdnikisegodhyaru, здесь мы ничего сделать не можем. if err != nil { return "", err } - // Если всё хорошо, надо обновить кэш + // Если всё хорошо, надо обновить кэш. if err := UpdatePsrCache(cfg, eTime, answer); err != nil { log.Errorf("%s", err) } @@ -64,21 +67,21 @@ func PsrClient(cfg *config.MyConfig) (string, error) { return answer, nil } - // Текущее время в Unix timestamp + // Текущее время в Unix timestamp. tsNowUnix := tsNow.Unix() - // Кэш просрочен + // Кэш просрочен. if tsCache < tsNowUnix { log.Debug("PSR cache miss") answer, err := PsrAPIClient(cfg) - // Здесь мы ничего сделать не можем + // Здесь мы ничего сделать не можем. if err != nil { return "", err } - // Если всё хорошо, надо обновить кэш + // Если всё хорошо, надо обновить кэш. if err := UpdatePsrCache(cfg, eTime, answer); err != nil { log.Errorf("%s", err) } @@ -86,8 +89,8 @@ func PsrClient(cfg *config.MyConfig) (string, error) { return answer, nil } - // TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше - // TODO: Научиться определять ситуацию, когда значением является unix timestamp + // TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше. + // TODO: Научиться определять ситуацию, когда значением является unix timestamp. log.Debug("PSR cache hit") 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) { var ( ctx = context.Background() - url = "https://prazdniki-segodnya.ru/" + link = "https://prazdniki-segodnya.ru/" respBody string ) - // Формальная попытка притвориться браузером + // Формальная попытка притвориться браузером. _, _, day := time.Now().Date() 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 { - return "", fmt.Errorf("unable to GET %s: %w", url, err) + return "", fmt.Errorf("unable to GET %s: %w", link, err) } var holidays []string - // Парсим body ответа от сервера + // Парсим body ответа от сервера. tokenizer := html.NewTokenizer(strings.NewReader(respBody)) for { @@ -122,7 +125,7 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) { err := tokenizer.Err() 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 } @@ -139,6 +142,7 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) { if tokenType == html.TextToken { str := tokenizer.Token().Data + holidays = append(holidays, strings.TrimSpace(str)) } } diff --git a/internal/randomfox/randomfox.go b/internal/randomfox/randomfox.go index 09eb8cb..300df96 100644 --- a/internal/randomfox/randomfox.go +++ b/internal/randomfox/randomfox.go @@ -1,3 +1,4 @@ +// Package randomfox implements randomfox.ca service client. package randomfox import ( @@ -10,11 +11,13 @@ import ( "github.com/carlmjohnson/requests" ) -// RandomFox структура, возвращаемая на запрос в randomfox.ca API. -type RandomFox struct { - Image string `json:"image,omitempty"` - Link string `json:"link"` -} +type ( + // RandomFox структура, возвращаемая на запрос в randomfox.ca API. + RandomFox struct { + Image string `json:"image,omitempty"` + Link string `json:"link"` + } +) // APIClient клиент сервиса randomfox.ca. func APIClient(cfg *config.MyConfig) (string, error) { diff --git a/internal/thecatapi/thecatapi.go b/internal/thecatapi/thecatapi.go index dfe41c2..f36435c 100644 --- a/internal/thecatapi/thecatapi.go +++ b/internal/thecatapi/thecatapi.go @@ -1,3 +1,4 @@ +// Package thecatapi implements api.thecatapi.com service client. package thecatapi import ( @@ -9,16 +10,18 @@ import ( "github.com/carlmjohnson/requests" ) -// TheCatAPIinnerStruct структурка, описывающая элемент массива, возвращемый Тhe cat api. -type TheCatAPIinnerStruct struct { - ID string `json:"id,omitempty"` - URL string `json:"url"` - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` -} +type ( + // TheCatAPIinnerStruct структурка, описывающая элемент массива, возвращемый Тhe cat api. + TheCatAPIinnerStruct struct { + ID string `json:"id,omitempty"` + URL string `json:"url"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + } -// TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API. -type TheCatAPI []TheCatAPIinnerStruct + // TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API. + TheCatAPI []TheCatAPIinnerStruct +) // APIClient клиент сервиса thecatapi.com. func APIClient(cfg *config.MyConfig) (string, error) { @@ -29,11 +32,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { cat TheCatAPI ) - err := requests. - URL(url). - UserAgent(userAgent). - ToJSON(&cat). - Fetch(ctx) + err := requests.URL(url).UserAgent(userAgent).ToJSON(&cat).Fetch(ctx) if err != nil { return "", fmt.Errorf("unable to GET %s: %w", url, err) diff --git a/internal/webapp/MsgParser.go b/internal/webapp/MsgParser.go index f951a09..1562384 100644 --- a/internal/webapp/MsgParser.go +++ b/internal/webapp/MsgParser.go @@ -1,8 +1,10 @@ +// Package webapp is main package implementing application logic. package webapp import ( "aleesa-webapp-go/internal/anekdotru" "aleesa-webapp-go/internal/bunicomic" + "aleesa-webapp-go/internal/celebratoday" "aleesa-webapp-go/internal/config" "aleesa-webapp-go/internal/flickr" "aleesa-webapp-go/internal/log" @@ -10,7 +12,6 @@ import ( "aleesa-webapp-go/internal/oboobs" "aleesa-webapp-go/internal/obutts" "aleesa-webapp-go/internal/openweathermap" - "aleesa-webapp-go/internal/prazdnikisegodnyaru" "aleesa-webapp-go/internal/randomfox" "aleesa-webapp-go/internal/thecatapi" "aleesa-webapp-go/internal/xkcdru" @@ -49,7 +50,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint return } - // Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение + // Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение. if j.Misc.Fwdcnt > cfg.ForwardsMax { 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 - // Классифицируем входящие сообщения. Первым делом, попробуем определить команды + // Классифицируем входящие сообщения. Первым делом, попробуем определить команды. if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign { var cmd = j.Message[len(j.Misc.Csign):] switch { case cmd == "cat" || cmd == "кис": - // Пытаемся вычитать ответ thecatapi 3 раза - for i := 0; i < 3; i++ { - answer, err = thecatapi.APIClient(cfg) + // Пытаемся вычитать ответ thecatapi 3 раза. + for i := range 3 { + answer, err = thecatapi.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 == "" { j.Message = "Нету кошечек, все разбежались." } else { - // for telegram return answer in markdown format + // for telegram return answer in markdown format. if j.Plugin == "telegram" { - cats := []string{"龴ↀ◡ↀ龴", "=^..^=", "≧◔◡◔≦", "^ↀᴥↀ^"} + cats := []string{"龴ↀ◡ↀ龴", "=^..^=", "≧◔◡◔≦", "^ↀᴥↀ^"} //nolint: gosmopolitan + j.Message = fmt.Sprintf("[%s](%s)", cats[rand.IntN(len(cats))], answer) } else { j.Message = answer } } + case cmd == "fox" || cmd == "лис": - // Пытаемся вычитать ответ randonfox.ca 3 раза - for i := 0; i < 3; i++ { - answer, err = randomfox.APIClient(cfg) + // Пытаемся вычитать ответ randonfox.ca 3 раза. + for i := range 3 { + answer, err = randomfox.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 } } + case cmd == "xkcd": - // Пытаемся вычитать ответ xkcd.ru 3 раза - for i := 0; i < 3; i++ { + // Пытаемся вычитать ответ xkcd.ru 3 раза. + for i := range 3 { answer, err = xkcdru.APIClient(cfg) if err != nil { @@ -139,10 +143,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint j.Message = answer } } + case cmd == "buni": - // Пытаемся вычитать ответ www.bunicomic.com 3 раза - for i := 0; i < 3; i++ { - answer, err = bunicomic.APIClient(cfg) + // Пытаемся вычитать ответ www.bunicomic.com 3 раза. + for i := range 3 { + answer, err = bunicomic.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 } } + case cmd == "anek" || cmd == "анек" || cmd == "анекдот": - // Пытаемся вычитать ответ www.anekdot.ru 3 раза - for i := 0; i < 3; i++ { - answer, err = anekdotru.APIClient(cfg) + // Пытаемся вычитать ответ www.anekdot.ru 3 раза. + for i := range 3 { + answer, err = anekdotru.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 } } + case cmd == "monkeyuser": - // Пытаемся вычитать ответ www.bunicomic.com 3 раза - for i := 0; i < 3; i++ { - answer, err = monkeyuser.APIClient(cfg) + // Пытаемся вычитать ответ monkeyuser.com 3 раза. + for i := range 3 { + answer, err = monkeyuser.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 } } + case cmd == "butt" || cmd == "booty" || cmd == "ass" || cmd == "попа" || cmd == "попка": - // Пытаемся вычитать ответ api.obutts.ru 3 раза - for i := 0; i < 3; i++ { - answer, err = obutts.APIClient(cfg) + // Пытаемся вычитать ответ api.obutts.ru 3 раза. + for i := range 3 { + answer, err = obutts.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 { if j.Plugin == "telegram" { arts := []string{"(__(__)", "(_!_)", "(__.__)"} + j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer) } else { j.Message = answer } } + case cmd == "titts" || cmd == "boobs" || cmd == "tities" || cmd == "boobies" || cmd == "сиси" || cmd == "сисечки": - // Пытаемся вычитать ответ api.oboobs.ru 3 раза - for i := 0; i < 3; i++ { - answer, err = oboobs.APIClient(cfg) + // Пытаемся вычитать ответ api.oboobs.ru 3 раза. + for i := range 3 { + answer, err = oboobs.APIClient(cfg) //nolint: contextcheck // Fuck off. if err != nil { 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 { if j.Plugin == "telegram" { arts := []string{"(. )( .)", "( . Y . )", "(o)(o)", "( @ )( @ )", "(.)(.)"} + j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer) } else { j.Message = answer } } + case cmd == "drink" || cmd == "праздник": - // Пытаемся вычитать ответ prazdniki-segodnya.ru 3 раза - for i := 0; i < 3; i++ { - answer, err = prazdnikisegodnyaru.PsrClient(cfg) + // Пытаемся вычитать ответ celebratoday.com 3 раза. + for i := range 3 { + answer, err = celebratoday.Client(cfg) //nolint: contextcheck // Fuck off. 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) continue @@ -283,8 +295,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint } case cmd == "frog" || cmd == "лягушка": - for i := 0; i < 3; i++ { - answer, err = flickr.SearchByTags(cfg, []string{"frog", "toad", "amphibian"}) + for i := range 3 { + answer, err = flickr.SearchByTags(cfg, []string{"frog", "toad", "amphibian"}) //nolint: contextcheck // Fuck off. if err != nil { 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. if j.Plugin == "telegram" { art := []string{"frog", "toad", "лягушка"} + 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 == "сыч": - for i := 0; i < 3; i++ { - answer, err = flickr.SearchByTags(cfg, []string{"owlet", "owl", "raptor", "bird of prey", "nocturnal bird", "barn owl", "tawny owl", "brown owl"}) + for i := range 3 { + 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 { 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 == "лошадка": - for i := 0; i < 3; i++ { - answer, err = flickr.SearchByTags(cfg, []string{"horse", "equine", "stallion", "mare", "steed", "thoroughbred"}) + for i := range 3 { + answer, err = flickr.SearchByTags(cfg, []string{"horse", "equine", "stallion", "mare", "steed", "thoroughbred"}) //nolint: contextcheck // Fuck off. if err != nil { 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. if j.Plugin == "telegram" { art := []string{"horse", "лошадь", "лошадка"} + 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 == "кролик": - for i := 0; i < 3; i++ { - answer, err = flickr.SearchByTags(cfg, []string{"bunny", "rabbit", "buck"}) + for i := range 3 { + answer, err = flickr.SearchByTags(cfg, []string{"bunny", "rabbit", "buck"}) //nolint: contextcheck // Fuck off. if err != nil { 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 == "улитка": - for i := 0; i < 3; i++ { - answer, err = flickr.SearchByTags(cfg, []string{"snail", "slug"}) + for i := range 3 { + answer, err = flickr.SearchByTags(cfg, []string{"snail", "slug"}) //nolint: contextcheck // Fuck off. if err != nil { 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. if j.Plugin == "telegram" { art := []string{"'-'_@_", "@╜", "@_'-'"} + 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 { if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil { log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err) + j.Message = "Я не знаю, какая погода в " + city } else { 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 answer, err := openweathermap.OwmClient(cfg, city, 5); err != nil { log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err) + j.Message = "Я не знаю, какая погода в " + city } else { j.Message = answer @@ -440,6 +457,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint } else { if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil { log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err) + j.Message = "Я не знаю, какая погода в " + city } else { j.Message = answer @@ -467,7 +485,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint return } - // Настало время формировать json и засылать его в дальше + // Настало время формировать json и засылать его в дальше. var message SMsg message.From = j.From @@ -493,7 +511,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint return } - // Заталкиваем наш json в редиску + // Заталкиваем наш json в редиску. if err := cfg.RedisClient.Publish(ctx, sendTo, data).Err(); err != nil { log.Warnf("Unable to send data to redis channel %s: %s", sendTo, err) } else { diff --git a/internal/webapp/ReadConfig.go b/internal/webapp/ReadConfig.go index 96c8475..69f50ea 100644 --- a/internal/webapp/ReadConfig.go +++ b/internal/webapp/ReadConfig.go @@ -25,12 +25,12 @@ func ReadConfig() error { for _, location := range locations { fileInfo, err := os.Stat(location) - // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём + // Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём. if err != nil { continue } - // Конфиг-файл длинноват для конфига, попробуем следующего кандидата + // Конфиг-файл длинноват для конфига, попробуем следующего кандидата. if fileInfo.Size() > ConfigFileSize { log.Warnf("Config file %s is too long for config, skipping", location) @@ -39,7 +39,7 @@ func ReadConfig() error { buf, err := os.ReadFile(location) - // Не удалось прочитать, попробуем следующего кандидата + // Не удалось прочитать, попробуем следующего кандидата. if err != nil { log.Warnf("Skip reading config file %s: %s", location, err) @@ -51,12 +51,12 @@ func ReadConfig() error { // сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется. var ( sampleConfig *config.MyConfig - tmp map[string]interface{} + tmp map[string]any ) err = hjson.Unmarshal(buf, &tmp) - // Не удалось распарсить - попробуем следующего кандидата + // Не удалось распарсить - попробуем следующего кандидата. if err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) @@ -65,7 +65,7 @@ func ReadConfig() error { tmpjson, err := json.Marshal(tmp) - // Не удалось преобразовать map-ку в json + // Не удалось преобразовать map-ку в json. if err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) @@ -78,7 +78,7 @@ func ReadConfig() error { continue } - // Валидируем значения из конфига + // Валидируем значения из конфига. if sampleConfig.Server == "" { sampleConfig.Server = Host } @@ -95,7 +95,7 @@ func ReadConfig() error { sampleConfig.Loglevel = Loglevel } - // sampleConfig.Log = "" if not set + // sampleConfig.Log = "" if not set. if sampleConfig.Channel == "" { log.Errorf("Channel field in config file %s must be set", location) @@ -129,7 +129,7 @@ func ReadConfig() error { continue } - // sampleConfig.OpenWeatherMap.Country is false by default + // sampleConfig.OpenWeatherMap.Country is false by default. // Теперь надо бы попробовать загрузить список User Agent-ов, с которыми ходить в разные апишки. uaFile := sampleConfig.DataDir + "/useragents.txt" diff --git a/internal/webapp/SigHandler.go b/internal/webapp/SigHandler.go index a86c55c..f36581b 100644 --- a/internal/webapp/SigHandler.go +++ b/internal/webapp/SigHandler.go @@ -21,14 +21,14 @@ func SigHandler(cfg *config.MyConfig) { case syscall.SIGQUIT: log.Info("Got SIGQUIT, quitting") - // Заходим на новую итерацию, если у нас "неинтересный" сигнал + // Заходим на новую итерацию, если у нас "неинтересный" сигнал. default: continue } Shutdown = true - // Отпишемся от всех каналов и закроем коннект к редиске + // Отпишемся от всех каналов и закроем коннект к редиске. if err = Subscriber.Unsubscribe(Ctx); err != nil { log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err) } diff --git a/internal/webapp/ValidateRmsg.go b/internal/webapp/ValidateRmsg.go index d0c85fd..5c51eb2 100644 --- a/internal/webapp/ValidateRmsg.go +++ b/internal/webapp/ValidateRmsg.go @@ -45,21 +45,21 @@ func ValidateRmsg(cfg *config.MyConfig, j RMsg, msg string) (RMsg, error) { return j, emsg } - // j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет - // j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка - // j.Misc.Csign если нам его не передали, возьмём значение из конфига + // j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет. + // j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка. + // j.Misc.Csign если нам его не передали, возьмём значение из конфига. if exist := j.Misc.Csign; exist == "" { j.Misc.Csign = cfg.Csign } - // j.Misc.Fwdcnt если нам его не передали, то будет 0 + // j.Misc.Fwdcnt если нам его не передали, то будет 0. if exist := j.Misc.Fwdcnt; exist == 0 { j.Misc.Fwdcnt = 1 } - // j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0 - // j.Misc.Msgformat может быть быть 1 или 0, по-умолчанию 0 - // j.Misc.Username можно не передавать, тогда будет пустая строка + // j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0. + // j.Misc.Msgformat может быть быть 1 или 0, по-умолчанию 0. + // j.Misc.Username можно не передавать, тогда будет пустая строка. return j, nil } diff --git a/internal/webapp/globals.go b/internal/webapp/globals.go index 16c011b..8aff271 100644 --- a/internal/webapp/globals.go +++ b/internal/webapp/globals.go @@ -33,7 +33,7 @@ var ( // Config структурка, содержащая данные конфига. Config *config.MyConfig - // ctx контекст редиски. + // Ctx контекст редиски. Ctx = context.Background() // Subscriber объектик сабскрайбера редиски. diff --git a/internal/webapp/types.go b/internal/webapp/types.go index 7d13d24..fc1f2f2 100644 --- a/internal/webapp/types.go +++ b/internal/webapp/types.go @@ -1,43 +1,45 @@ package webapp -// RMsg структурка, описывающая входящее сообщение из pubsub-канала redis-ки. -type RMsg struct { - From string `json:"from,omitempty"` - Chatid string `json:"chatid,omitempty"` - Userid string `json:"userid,omitempty"` - Threadid string `json:"threadid,omitempty"` - Message string `json:"message,omitempty"` - Plugin string `json:"plugin,omitempty"` - Mode string `json:"mode,omitempty"` - Misc struct { - Answer int64 `json:"answer,omitempty"` - Botnick string `json:"bot_nick,omitempty"` - Csign string `json:"csign,omitempty"` - Fwdcnt int64 `json:"fwd_cnt,omitempty"` - GoodMorning int64 `json:"good_morning,omitempty"` - Msgformat int64 `json:"msg_format,omitempty"` - Username string `json:"username,omitempty"` - } `json:"Misc"` -} +type ( + // RMsg структурка, описывающая входящее сообщение из pubsub-канала redis-ки. + RMsg struct { + From string `json:"from,omitempty"` + Chatid string `json:"chatid,omitempty"` + Userid string `json:"userid,omitempty"` + Threadid string `json:"threadid,omitempty"` + Message string `json:"message,omitempty"` + Plugin string `json:"plugin,omitempty"` + Mode string `json:"mode,omitempty"` + Misc struct { //nolint: govet // Мне так удобнее. + Answer int64 `json:"answer,omitempty"` + Botnick string `json:"bot_nick,omitempty"` + Csign string `json:"csign,omitempty"` + Fwdcnt int64 `json:"fwd_cnt,omitempty"` + GoodMorning int64 `json:"good_morning,omitempty"` + Msgformat int64 `json:"msg_format,omitempty"` + Username string `json:"username,omitempty"` + } `json:"Misc"` + } -// SMsg структурка, описывающая исходящее сообщение в pubsub-канал redis-ки. -type SMsg struct { - From string `json:"from"` - Chatid string `json:"chatid"` - Userid string `json:"userid"` - Threadid string `json:"threadid"` - Message string `json:"message"` - Plugin string `json:"plugin"` - Mode string `json:"mode"` - Misc struct { - Answer int64 `json:"answer"` - Botnick string `json:"bot_nick"` - Csign string `json:"csign"` - Fwdcnt int64 `json:"fwd_cnt"` - GoodMorning int64 `json:"good_morning"` - Msgformat int64 `json:"msg_format"` - Username string `json:"username"` - } `json:"misc"` -} + // SMsg структурка, описывающая исходящее сообщение в pubsub-канал redis-ки. + SMsg struct { + From string `json:"from"` + Chatid string `json:"chatid"` + Userid string `json:"userid"` + Threadid string `json:"threadid"` + Message string `json:"message"` + Plugin string `json:"plugin"` + Mode string `json:"mode"` + Misc struct { //nolint: govet // Мне так удобнее. + Answer int64 `json:"answer"` + Botnick string `json:"bot_nick"` + Csign string `json:"csign"` + Fwdcnt int64 `json:"fwd_cnt"` + GoodMorning int64 `json:"good_morning"` + Msgformat int64 `json:"msg_format"` + Username string `json:"username"` + } `json:"misc"` + } +) /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/xkcdru/xkcdru.go b/internal/xkcdru/xkcdru.go index f660ba5..997a548 100644 --- a/internal/xkcdru/xkcdru.go +++ b/internal/xkcdru/xkcdru.go @@ -1,3 +1,4 @@ +// Package xkcdru provides api to xkcd.ru. package xkcdru 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 { return "", err @@ -48,7 +49,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { } }(resp.Body) - // Response body нам не интересен, нам интересен статус и нам интересен заголовок Location + // Response body нам не интересен, нам интересен статус и нам интересен заголовок Location. if resp.StatusCode == 302 { location := resp.Header.Get("Location")