replace email processing reactions; update deps

This commit is contained in:
Aine
2024-04-30 09:18:04 +03:00
parent 15d61f174e
commit 0e3655195a
35 changed files with 709 additions and 247 deletions

View File

@@ -1,15 +1,37 @@
# healthchecks
A [healthchecks.io](https://github.com/healthchecks/healthchecks) client
A fully async [healthchecks.io](https://github.com/healthchecks/healthchecks) golang client, with lots of features, some highlights:
check the godoc for information
* Highly configurable: `WithHTTPClient()`, `WithBaseURL()`, `WithUserAgent()`, `WithErrLog()`, `WithCheckUUID()`, `WithAutoProvision()`, etc.
* Automatic determination of HTTP method (`POST`, `HEAD`) based on body existence
* Auto mode: just call `client.Auto(time.Duration)` and client will send `Success()` request automatically with specified frequency
* Global mode: init client once with `healthchecks.New()`, and access it from anywhere by calling `healthchecks.Global()`
Check [godoc](https://pkg.go.dev/gitlab.com/etke.cc/go/healthchecks/v2) for more details.
```go
hc := healthchecks.New(
healthchecks.WithCheckUUID("your-uuid"),
)
go hc.Auto()
package main
hc.Log(strings.NewReader("optional body you can attach to any action"))
hc.Shutdown()
import (
"time"
"gitlab.com/etke.cc/go/healthchecks/v2"
)
var hc *healthchecks.Client
func main() {
hc = healthchecks.New(
healthchecks.WithCheckUUID("CHECK_UUID")
)
defer hc.Shutdown()
// send basic success request
hc.Success()
// or use auto mode, that will send success request with the specified frequency
go hc.Auto(1*time.Minute)
// need to call the client from another place in your project?
// just call healthchecks.Global() and you will get the same client
}
```

View File

@@ -17,8 +17,3 @@ func (c *Client) Auto(every time.Duration) {
}
}
}
// Shutdown the client
func (c *Client) Shutdown() {
c.done <- true
}

View File

@@ -5,39 +5,46 @@ import (
"io"
"net/http"
"strconv"
"sync"
"time"
"github.com/google/uuid"
)
// Client for healthchecks
// if client initialized without any options, it will be disabled by default,
// but you can override it by calling SetEnabled(true).
type Client struct {
http *http.Client
log func(string, error)
baseURL string
uuid string
rid string
create bool
done chan bool
wg sync.WaitGroup
enabled bool
http *http.Client
log func(string, error)
userAgent string
baseURL string
uuid string
rid string
create bool
done chan bool
}
// init client
func (c *Client) init(options ...Option) {
c.enabled = true
c.log = DefaultErrLog
c.baseURL = DefaultAPI
c.userAgent = DefaultUserAgent
c.http = &http.Client{Timeout: 10 * time.Second}
c.done = make(chan bool, 1)
c.uuid = ""
if len(options) == 0 {
c.enabled = false
}
for _, option := range options {
option(c)
}
if c.log == nil {
c.log = DefaultErrLog
}
if c.baseURL == "" {
c.baseURL = DefaultAPI
}
if c.http == nil {
c.http = &http.Client{Timeout: 10 * time.Second}
}
if c.done == nil {
c.done = make(chan bool, 1)
}
if c.uuid == "" {
randomUUID, _ := uuid.NewRandom()
c.uuid = randomUUID.String()
@@ -46,22 +53,39 @@ func (c *Client) init(options ...Option) {
}
}
// call API
func (c *Client) call(operation, endpoint string, body ...io.Reader) {
var err error
var resp *http.Response
if !c.enabled {
return
}
c.wg.Add(1)
defer c.wg.Done()
targetURL := fmt.Sprintf("%s/%s%s?rid=%s", c.baseURL, c.uuid, endpoint, c.rid)
if c.create {
targetURL += "&create=1"
}
var req *http.Request
var err error
if len(body) > 0 {
resp, err = c.http.Post(targetURL, "text/plain; charset=utf-8", body[0])
req, err = http.NewRequest(http.MethodPost, targetURL, body[0])
} else {
resp, err = c.http.Head(targetURL)
req, err = http.NewRequest(http.MethodHead, targetURL, http.NoBody)
}
if err != nil {
c.log(operation, err)
return
}
req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
resp, err := c.http.Do(req)
if err != nil {
c.log(operation, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
@@ -76,27 +100,40 @@ func (c *Client) call(operation, endpoint string, body ...io.Reader) {
}
}
// SetEnabled sets the enabled flag, ignoring the options
// if client initialized without any options, it will be disabled by default,
// but you can override it by calling SetEnabled(true).
func (c *Client) SetEnabled(enabled bool) {
c.enabled = enabled
}
// Start signal means the job started
func (c *Client) Start(optionalBody ...io.Reader) {
c.call("start", "/start", optionalBody...)
go c.call("start", "/start", optionalBody...)
}
// Success signal means the job has completed successfully (or, a continuously running process is still running and healthy).
func (c *Client) Success(optionalBody ...io.Reader) {
c.call("success", "", optionalBody...)
go c.call("success", "", optionalBody...)
}
// Fail signal means the job failed
func (c *Client) Fail(optionalBody ...io.Reader) {
c.call("fail", "/fail", optionalBody...)
go c.call("fail", "/fail", optionalBody...)
}
// Log signal just adds an event to the job log, without changing job status
func (c *Client) Log(optionalBody ...io.Reader) {
c.call("log", "/log", optionalBody...)
go c.call("log", "/log", optionalBody...)
}
// ExitStatus signal sends job's exit code (0-255)
func (c *Client) ExitStatus(exitCode int, optionalBody ...io.Reader) {
c.call("exit status", "/"+strconv.Itoa(exitCode), optionalBody...)
go c.call("exit status", "/"+strconv.Itoa(exitCode), optionalBody...)
}
// Shutdown the client
func (c *Client) Shutdown() {
c.done <- true
c.wg.Wait()
}

View File

@@ -6,12 +6,19 @@ import (
"github.com/google/uuid"
)
// DefaultAPI base url for checks
const DefaultAPI = "https://hc-ping.com"
const (
// DefaultAPI base url for checks
DefaultAPI = "https://hc-ping.com"
// DefaultUserAgent for the client
DefaultUserAgent = "Go-Healthchecks (lib; +https://gitlab.com/etke.cc/go/healthchecks)"
)
// ErrLog used to log errors occurred during an operation
type ErrLog func(operation string, err error)
// global client
var global *Client
// DefaultErrLog if you don't provide one yourself
var DefaultErrLog = func(operation string, err error) {
fmt.Printf("healtchecks operation %q failed: %v\n", operation, err)
@@ -25,5 +32,18 @@ func New(options ...Option) *Client {
}
c.init(options...)
if global == nil {
global = c
}
return c
}
// Global healthchecks client
func Global() *Client {
if global == nil {
global = New()
}
return global
}

View File

@@ -2,6 +2,7 @@ package healthchecks
import "net/http"
// Option for healthchecks client
type Option func(*Client)
// WithHTTPClient sets the http client
@@ -18,6 +19,13 @@ func WithBaseURL(baseURL string) Option {
}
}
// WithUserAgent sets the user agent
func WithUserAgent(userAgent string) Option {
return func(c *Client) {
c.userAgent = userAgent
}
}
// WithErrLog sets the error log
func WithErrLog(errLog ErrLog) Option {
return func(c *Client) {
@@ -39,6 +47,13 @@ func WithAutoProvision() Option {
}
}
// WithGlobal sets this client as the global client
func WithGlobal() Option {
return func(c *Client) {
global = c
}
}
// WithDone sets the done channel
func WithDone(done chan bool) Option {
return func(c *Client) {

View File

@@ -53,13 +53,17 @@ func (p *Client) GetWithContext(ctx context.Context, identifier string) ([]*Targ
return nil, err
}
req.SetBasicAuth(p.login, p.password)
req.Header.Set("User-Agent", "Go-psd-client/"+version)
req.Header.Set("User-Agent", "Go-PSD-client/"+version)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusGone { // not found, to distinguish from reverse proxy 404 error
return nil, nil
}
err = fmt.Errorf("%s", resp.Status) //nolint:goerr113 // that's ok
return nil, err
}

View File

@@ -1,85 +1,132 @@
run:
concurrency: 4
timeout: 5m
timeout: 30m
issues-exit-code: 1
tests: true
build-tags: []
skip-dirs: []
skip-dirs-use-default: true
skip-files: []
modules-download-mode: readonly
allow-parallel-runners: false
output:
format: colored-line-number
formats:
- format: colored-line-number
print-issued-lines: true
print-linter-name: true
uniq-by-line: true
path-prefix: ""
sort-results: true
linters-settings:
decorder:
dec-order:
- const
- var
- type
- func
dogsled:
max-blank-identifiers: 3
errcheck:
check-type-assertions: true
check-blank: true
gci:
local-prefixes: gitlab.com/etke.cc/linkpearl
errchkjson:
report-no-exported: true
exhaustive:
check:
- switch
- map
default-signifies-exhaustive: true
gocognit:
min-complexity: 10
min-complexity: 15
nestif:
min-complexity: 4
min-complexity: 5
gocritic:
enabled-tags:
- diagnostic
- style
- performance
gofmt:
simplify: true
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'
gofumpt:
lang-version: "1.19"
goimports:
local-prefixes: gitlab.com/etke.cc/linkpearl
gosimple:
go: "1.19"
checks: [ "all" ]
govet:
check-shadowing: true
enable:
- atomicalign
- shadow
extra-rules: true
grouper:
const-require-single-const: true
import-require-single-import: true
var-require-single-var: true
misspell:
locale: US
staticcheck:
go: "1.19"
checks: [ "all" ]
stylecheck:
go: "1.19"
usestdlibvars:
time-month: true
time-layout: true
crypto-hash: true
default-rpc-path: true
os-dev-null: true
sql-isolation-level: true
tls-signature-scheme: true
constant-kind: true
unparam:
check-exported: true
unused:
go: "1.19"
linters:
disable-all: false
enable:
- megacheck
- govet
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- gci
- errchkjson
- errname
- errorlint
- execinquery
- exhaustive
- exportloopref
- forcetypeassert
- gocognit
- nestif
- gocritic
- gocyclo
- gofmt
- gofumpt
- goimports
- gosec
- gosimple
- gosmopolitan
- govet
- ineffassign
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- staticcheck
- stylecheck
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
fast: false
issues:
exclude-dirs:
- mocks
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- dupl
- gosec
@@ -89,6 +136,9 @@ issues:
- linters:
- lll
source: "^//go:generate "
- linters:
- revive
text: "returns unexported type"
max-issues-per-linter: 0
max-same-issues: 0
new: false

View File

@@ -15,6 +15,12 @@ type RespThreads struct {
NextBatch string `json:"next_batch"`
}
// RespRelations is response of https://spec.matrix.org/v1.8/client-server-api/#get_matrixclientv1roomsroomidrelationseventidreltype
type RespRelations struct {
Chunk []*event.Event `json:"chunk"`
NextBatch string `json:"next_batch"`
}
// Threads endpoint, ref: https://spec.matrix.org/v1.8/client-server-api/#get_matrixclientv1roomsroomidthreads
func (l *Linkpearl) Threads(ctx context.Context, roomID id.RoomID, fromToken ...string) (*RespThreads, error) {
var from string
@@ -33,8 +39,26 @@ func (l *Linkpearl) Threads(ctx context.Context, roomID id.RoomID, fromToken ...
return resp, UnwrapError(err)
}
// Relations returns all relations of the given type for the given event
func (l *Linkpearl) Relations(ctx context.Context, roomID id.RoomID, eventID id.EventID, relType string, fromToken ...string) (*RespRelations, error) {
var from string
if len(fromToken) > 0 {
from = fromToken[0]
}
query := map[string]string{
"from": from,
"limit": "100",
}
var resp *RespRelations
urlPath := l.GetClient().BuildURLWithQuery(mautrix.ClientURLPath{"v1", "rooms", roomID, "relations", eventID, relType}, query)
_, err := l.GetClient().MakeRequest(ctx, "GET", urlPath, nil, &resp)
return resp, UnwrapError(err)
}
// FindThreadBy tries to find thread message event by field and value
func (l *Linkpearl) FindThreadBy(ctx context.Context, roomID id.RoomID, field, value string, fromToken ...string) *event.Event {
func (l *Linkpearl) FindThreadBy(ctx context.Context, roomID id.RoomID, fieldValue map[string]string, fromToken ...string) *event.Event {
var from string
if len(fromToken) > 0 {
from = fromToken[0]
@@ -48,9 +72,11 @@ func (l *Linkpearl) FindThreadBy(ctx context.Context, roomID id.RoomID, field, v
}
for _, msg := range resp.Chunk {
evt, contains := l.eventContains(ctx, msg, field, value)
if contains {
return evt
for field, value := range fieldValue {
evt, contains := l.eventContains(ctx, msg, field, value)
if contains {
return evt
}
}
}
@@ -58,11 +84,11 @@ func (l *Linkpearl) FindThreadBy(ctx context.Context, roomID id.RoomID, field, v
return nil
}
return l.FindThreadBy(ctx, roomID, field, value, resp.NextBatch)
return l.FindThreadBy(ctx, roomID, fieldValue, resp.NextBatch)
}
// FindEventBy tries to find message event by field and value
func (l *Linkpearl) FindEventBy(ctx context.Context, roomID id.RoomID, field, value string, fromToken ...string) *event.Event {
func (l *Linkpearl) FindEventBy(ctx context.Context, roomID id.RoomID, fieldValue map[string]string, fromToken ...string) *event.Event {
var from string
if len(fromToken) > 0 {
from = fromToken[0]
@@ -76,9 +102,11 @@ func (l *Linkpearl) FindEventBy(ctx context.Context, roomID id.RoomID, field, va
}
for _, msg := range resp.Chunk {
evt, contains := l.eventContains(ctx, msg, field, value)
if contains {
return evt
for field, value := range fieldValue {
evt, contains := l.eventContains(ctx, msg, field, value)
if contains {
return evt
}
}
}
@@ -86,7 +114,7 @@ func (l *Linkpearl) FindEventBy(ctx context.Context, roomID id.RoomID, field, va
return nil
}
return l.FindEventBy(ctx, roomID, field, value, resp.End)
return l.FindEventBy(ctx, roomID, fieldValue, resp.End)
}
func (l *Linkpearl) eventContains(ctx context.Context, evt *event.Event, field, value string) (*event.Event, bool) {

View File

@@ -21,6 +21,8 @@ const (
DefaultAccountDataCache = 1000
// DefaultEventsLimit for methods like lp.Threads() and lp.FindEventBy()
DefaultEventsLimit = 1000
// DefaultTypingTimeout in seconds for typing notifications
DefaultTypingTimeout = 60
)
// Linkpearl object
@@ -104,7 +106,7 @@ func New(cfg *Config) (*Linkpearl, error) {
return nil, err
}
lp.ch.LoginAs = cfg.LoginAs()
if err = lp.ch.Init(context.Background()); err != nil {
if err := lp.ch.Init(context.Background()); err != nil {
return nil, err
}
lp.api.Crypto = lp.ch
@@ -140,33 +142,33 @@ func (l *Linkpearl) SetPresence(ctx context.Context, presence event.Presence, me
return err
}
// SetJoinPermit sets the the join permit callback function
// SetJoinPermit sets the join permit callback function
func (l *Linkpearl) SetJoinPermit(value func(context.Context, *event.Event) bool) {
l.joinPermit = value
}
// Start performs matrix /sync
func (l *Linkpearl) Start(optionalStatusMsg ...string) error {
func (l *Linkpearl) Start(ctx context.Context, optionalStatusMsg ...string) error {
l.initSync()
var statusMsg string
if len(optionalStatusMsg) > 0 {
statusMsg = optionalStatusMsg[0]
}
err := l.SetPresence(context.Background(), event.PresenceOnline, statusMsg)
err := l.SetPresence(ctx, event.PresenceOnline, statusMsg)
if err != nil {
l.log.Error().Err(err).Msg("cannot set presence")
}
defer l.Stop()
defer l.Stop(ctx)
l.log.Info().Msg("client has been started")
return l.api.Sync()
return l.api.SyncWithContext(ctx)
}
// Stop the client
func (l *Linkpearl) Stop() {
func (l *Linkpearl) Stop(ctx context.Context) {
l.log.Debug().Msg("stopping the client")
if err := l.api.SetPresence(context.Background(), event.PresenceOffline); err != nil {
if err := l.api.SetPresence(ctx, event.PresenceOffline); err != nil {
l.log.Error().Err(err).Msg("cannot set presence")
}
l.api.StopSync()

71
vendor/gitlab.com/etke.cc/linkpearl/reactions.go generated vendored Normal file
View File

@@ -0,0 +1,71 @@
package linkpearl
import (
"context"
"maunium.net/go/mautrix/id"
)
// reactionPrefix is the prefix for all reaction in account data
const reactionPrefix = "cc.etke.linkpearl.reaction."
// SendReaction sends a reaction to a message
func (l *Linkpearl) SendReaction(ctx context.Context, roomID id.RoomID, eventID id.EventID, reaction string) error {
// Check if the reaction already exists
if l.getReactionAD(ctx, roomID, eventID, reaction) != "" {
return nil
}
resp, err := l.GetClient().SendReaction(ctx, roomID, eventID, reaction)
if err != nil {
return err
}
return l.updateReactionsAD(ctx, roomID, eventID, reaction, resp.EventID)
}
// RedactReaction redacts a reaction from a message
func (l *Linkpearl) RedactReaction(ctx context.Context, roomID id.RoomID, eventID id.EventID, reaction string) error {
existingID := l.getReactionAD(ctx, roomID, eventID, reaction)
// Check if the reaction already exists
if existingID == "" {
return nil
}
if _, err := l.GetClient().RedactEvent(ctx, roomID, id.EventID(existingID)); err != nil {
return err
}
return l.updateReactionsAD(ctx, roomID, eventID, reaction, "")
}
// ReplaceReaction replaces a reaction with another
func (l *Linkpearl) ReplaceReaction(ctx context.Context, roomID id.RoomID, eventID id.EventID, oldReaction, newReaction string) error {
if err := l.RedactReaction(ctx, roomID, eventID, oldReaction); err != nil {
return err
}
return l.SendReaction(ctx, roomID, eventID, newReaction)
}
func (l *Linkpearl) getReactionAD(ctx context.Context, roomID id.RoomID, eventID id.EventID, reaction string) string {
adID := reactionPrefix + eventID.String()
existing, err := l.GetRoomAccountData(ctx, roomID, adID)
if err != nil {
l.log.Error().Err(err).Msg("failed to get existing reactions")
return ""
}
return existing[reaction]
}
func (l *Linkpearl) updateReactionsAD(ctx context.Context, roomID id.RoomID, eventID id.EventID, reaction string, reactionID id.EventID) error {
adID := reactionPrefix + eventID.String()
existing, err := l.GetRoomAccountData(ctx, roomID, adID)
if err != nil {
return err
}
if reactionID == "" {
delete(existing, reaction)
} else {
existing[reaction] = reactionID.String()
}
return l.SetRoomAccountData(ctx, roomID, adID, existing)
}

View File

@@ -2,6 +2,7 @@ package linkpearl
import (
"context"
"time"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@@ -12,7 +13,7 @@ import (
// Send a message to the roomID and automatically try to encrypt it, if the destination room is encrypted
//
//nolint:unparam // it's public interface
func (l *Linkpearl) Send(ctx context.Context, roomID id.RoomID, content interface{}) (id.EventID, error) {
func (l *Linkpearl) Send(ctx context.Context, roomID id.RoomID, content any) (id.EventID, error) {
l.log.Debug().Str("roomID", roomID.String()).Any("content", content).Msg("sending event")
resp, err := l.api.SendMessageEvent(ctx, roomID, event.EventMessage, content)
if err != nil {
@@ -21,6 +22,18 @@ func (l *Linkpearl) Send(ctx context.Context, roomID id.RoomID, content interfac
return resp.EventID, nil
}
// SendTyping notification
func (l *Linkpearl) SendTyping(ctx context.Context, roomID id.RoomID, typing bool, timeout ...int) {
ttl := DefaultTypingTimeout
if len(timeout) > 0 {
ttl = timeout[0]
}
_, err := l.api.UserTyping(ctx, roomID, typing, time.Duration(ttl)*time.Second)
if err != nil {
l.log.Warn().Err(err).Bool("typing", typing).Msg("cannot set typing")
}
}
// SendNotice to a room with optional relations, markdown supported
func (l *Linkpearl) SendNotice(ctx context.Context, roomID id.RoomID, message string, relates ...*event.RelatesTo) {
var withRelatesTo bool

View File

@@ -13,27 +13,27 @@ import (
// OnEventType allows callers to be notified when there are new events for the given event type.
// There are no duplicate checks.
func (l *Linkpearl) OnEventType(eventType event.Type, callback mautrix.EventHandler) {
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType(eventType, callback)
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType(eventType, callback) //nolint:forcetypeassert // we know it's an ExtensibleSyncer
}
// OnSync shortcut to mautrix.DefaultSyncer.OnSync
func (l *Linkpearl) OnSync(callback mautrix.SyncHandler) {
l.api.Syncer.(mautrix.ExtensibleSyncer).OnSync(callback)
l.api.Syncer.(mautrix.ExtensibleSyncer).OnSync(callback) //nolint:forcetypeassert // we know it's an ExtensibleSyncer
}
// OnEvent shortcut to mautrix.DefaultSyncer.OnEvent
func (l *Linkpearl) OnEvent(callback mautrix.EventHandler) {
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEvent(callback)
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEvent(callback) //nolint:forcetypeassert // we know it's an ExtensibleSyncer
}
func (l *Linkpearl) initSync() {
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType(
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType( //nolint:forcetypeassert // we know it's an ExtensibleSyncer
event.StateEncryption,
func(ctx context.Context, evt *event.Event) {
go l.onEncryption(ctx, evt)
},
)
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType(
l.api.Syncer.(mautrix.ExtensibleSyncer).OnEventType( //nolint:forcetypeassert // we know it's an ExtensibleSyncer
event.StateMember,
func(ctx context.Context, evt *event.Event) {
go l.onMembership(ctx, evt)

View File

@@ -1,6 +1,8 @@
package linkpearl
import (
"errors"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@@ -120,26 +122,12 @@ func ParseContent(evt *event.Event, log *zerolog.Logger) {
// UnwrapError tries to unwrap a error into something meaningful, like mautrix.HTTPError or mautrix.RespError
func UnwrapError(err error) error {
switch err.(type) {
case nil:
return nil
case mautrix.HTTPError:
return unwrapHTTPError(err)
default:
return err
var httpErr mautrix.HTTPError
if errors.As(err, &httpErr) {
uwerr := httpErr.Unwrap()
if uwerr != nil {
return uwerr
}
}
}
func unwrapHTTPError(err error) error {
httperr, ok := err.(mautrix.HTTPError)
if !ok {
return err
}
uwerr := httperr.Unwrap()
if uwerr != nil {
return uwerr
}
return httperr
return err
}