File: teams_config.go

package info (click to toggle)
golang-github-nicholas-fedor-shoutrrr 0.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 4,432 kB
  • sloc: sh: 74; makefile: 5
file content (203 lines) | stat: -rw-r--r-- 5,514 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package teams

import (
	"fmt"
	"net/url"
	"regexp"
	"strings"

	"github.com/nicholas-fedor/shoutrrr/pkg/format"
	"github.com/nicholas-fedor/shoutrrr/pkg/services/standard"
	"github.com/nicholas-fedor/shoutrrr/pkg/types"
)

// Scheme is the identifier for the Teams service protocol.
const Scheme = "teams"

// Config constants.
const (
	DummyURL           = "teams://dummy@dummy.com" // Default placeholder URL
	ExpectedOrgMatches = 2                         // Full match plus organization domain capture group
	MinPathComponents  = 3                         // Minimum required path components: AltID, GroupOwner, ExtraID
)

// Config represents the configuration for the Teams service.
type Config struct {
	standard.EnumlessConfig
	Group      string `optional:"" url:"user"`
	Tenant     string `optional:"" url:"host"`
	AltID      string `optional:"" url:"path1"`
	GroupOwner string `optional:"" url:"path2"`
	ExtraID    string `optional:"" url:"path3"`

	Title string `key:"title" optional:""`
	Color string `key:"color" optional:""`
	Host  string `key:"host"  optional:""` // Required, no default
}

// WebhookParts returns the webhook components as an array.
func (config *Config) WebhookParts() [5]string {
	return [5]string{config.Group, config.Tenant, config.AltID, config.GroupOwner, config.ExtraID}
}

// SetFromWebhookURL updates the Config from a Teams webhook URL.
func (config *Config) SetFromWebhookURL(webhookURL string) error {
	orgPattern := regexp.MustCompile(
		`https://([a-zA-Z0-9-\.]+)` + WebhookDomain + `/` + Path + `/([0-9a-f\-]{36})@([0-9a-f\-]{36})/` + ProviderName + `/([0-9a-f]{32})/([0-9a-f\-]{36})/([^/]+)`,
	)

	orgGroups := orgPattern.FindStringSubmatch(webhookURL)
	if len(orgGroups) != ExpectedComponents {
		return ErrInvalidWebhookFormat
	}

	config.Host = orgGroups[1] + ".webhook.office.com"

	parts, err := ParseAndVerifyWebhookURL(webhookURL)
	if err != nil {
		return err
	}

	config.setFromWebhookParts(parts)

	return nil
}

// ConfigFromWebhookURL creates a new Config from a parsed Teams webhook URL.
func ConfigFromWebhookURL(webhookURL url.URL) (*Config, error) {
	webhookURL.RawQuery = ""
	config := &Config{Host: webhookURL.Host}

	if err := config.SetFromWebhookURL(webhookURL.String()); err != nil {
		return nil, err
	}

	return config, nil
}

// GetURL constructs a URL from the Config fields.
func (config *Config) GetURL() *url.URL {
	resolver := format.NewPropKeyResolver(config)

	return config.getURL(&resolver)
}

// getURL constructs a URL using the provided resolver.
func (config *Config) getURL(resolver types.ConfigQueryResolver) *url.URL {
	if config.Host == "" {
		return nil
	}

	return &url.URL{
		User:     url.User(config.Group),
		Host:     config.Tenant,
		Path:     "/" + config.AltID + "/" + config.GroupOwner + "/" + config.ExtraID,
		Scheme:   Scheme,
		RawQuery: format.BuildQuery(resolver),
	}
}

// SetURL updates the Config from a URL.
func (config *Config) SetURL(url *url.URL) error {
	resolver := format.NewPropKeyResolver(config)

	return config.setURL(&resolver, url)
}

// setURL updates the Config from a URL using the provided resolver.
// It parses the URL parts, sets query parameters, and ensures the host is specified.
// Returns an error if the URL is invalid or the host is missing.
func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error {
	parts, err := parseURLParts(url)
	if err != nil {
		return err
	}

	config.setFromWebhookParts(parts)

	if err := config.setQueryParams(resolver, url.Query()); err != nil {
		return err
	}

	// Allow dummy URL during documentation generation
	if config.Host == "" && (url.User != nil && url.User.Username() == "dummy") {
		config.Host = "dummy.webhook.office.com"
	} else if config.Host == "" {
		return ErrMissingHostParameter
	}

	return nil
}

// parseURLParts extracts and validates webhook components from a URL.
func parseURLParts(url *url.URL) ([5]string, error) {
	var parts [5]string
	if url.String() == DummyURL {
		return parts, nil
	}

	pathParts := strings.Split(url.Path, "/")
	if pathParts[0] == "" {
		pathParts = pathParts[1:]
	}

	if len(pathParts) < MinPathComponents {
		return parts, ErrMissingExtraIDComponent
	}

	parts = [5]string{
		url.User.Username(),
		url.Hostname(),
		pathParts[0],
		pathParts[1],
		pathParts[2],
	}
	if err := verifyWebhookParts(parts); err != nil {
		return parts, fmt.Errorf("invalid URL format: %w", err)
	}

	return parts, nil
}

// setQueryParams applies query parameters to the Config using the resolver.
// It resets Color, Host, and Title, then updates them based on query values.
// Returns an error if the resolver fails to set any parameter.
func (config *Config) setQueryParams(resolver types.ConfigQueryResolver, query url.Values) error {
	config.Color = ""
	config.Host = ""
	config.Title = ""

	for key, vals := range query {
		if len(vals) > 0 && vals[0] != "" {
			switch key {
			case "color":
				config.Color = vals[0]
			case "host":
				config.Host = vals[0]
			case "title":
				config.Title = vals[0]
			}

			if err := resolver.Set(key, vals[0]); err != nil {
				return fmt.Errorf(
					"%w: key=%q, value=%q: %w",
					ErrSetParameterFailed,
					key,
					vals[0],
					err,
				)
			}
		}
	}

	return nil
}

// setFromWebhookParts sets Config fields from webhook parts.
func (config *Config) setFromWebhookParts(parts [5]string) {
	config.Group = parts[0]
	config.Tenant = parts[1]
	config.AltID = parts[2]
	config.GroupOwner = parts[3]
	config.ExtraID = parts[4]
}