~gbmor/getwtxt

ref: c37aa835d01010af1c486dec6625717a50472fc8 getwtxt/registry/user.go -rw-r--r-- 6.8 KiB
c37aa835Ben Morrison fixed test case for fetching remote registry data 1 year, 3 months ago
                                                                                
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
Copyright (c) 2019 Ben Morrison (gbmor)

This file is part of Registry.

Registry is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Registry is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Registry.  If not, see <https://www.gnu.org/licenses/>.
*/

package registry // import "git.sr.ht/~gbmor/getwtxt/registry"

import (
	"fmt"
	"net"
	"strings"
	"sync"
	"time"
)

// AddUser inserts a new user into the Registry.
func (registry *Registry) AddUser(nickname, urlKey string, ipAddress net.IP, statuses TimeMap) error {

	if registry == nil {
		return fmt.Errorf("can't add user to uninitialized registry")

	} else if nickname == "" || urlKey == "" {
		return fmt.Errorf("both URL and Nick must be specified")

	} else if !strings.HasPrefix(urlKey, "http") {
		return fmt.Errorf("invalid URL: %v", urlKey)
	}

	registry.Mu.Lock()
	defer registry.Mu.Unlock()

	if _, ok := registry.Users[urlKey]; ok {
		return fmt.Errorf("user %v already exists", urlKey)
	}

	registry.Users[urlKey] = &User{
		Mu:           sync.RWMutex{},
		Nick:         nickname,
		URL:          urlKey,
		LastModified: "",
		IP:           ipAddress,
		Date:         time.Now().Format(time.RFC3339),
		Status:       statuses}

	return nil
}

// Put inserts a given User into an Registry. The User
// being pushed need only have the URL field filled.
// All other fields may be empty.
// This can be destructive: an existing User in the
// Registry will be overwritten if its User.URL is the
// same as the User.URL being pushed.
func (registry *Registry) Put(user *User) error {
	if user == nil {
		return fmt.Errorf("can't push nil data to registry")
	}
	if registry == nil || registry.Users == nil {
		return fmt.Errorf("can't push data to registry: registry uninitialized")
	}
	user.Mu.RLock()
	if user.URL == "" {
		user.Mu.RUnlock()
		return fmt.Errorf("can't push data to registry: missing URL for key")
	}
	urlKey := user.URL
	registry.Mu.Lock()
	registry.Users[urlKey] = user
	registry.Mu.Unlock()
	user.Mu.RUnlock()

	return nil
}

// Get returns the User associated with the
// provided URL key in the Registry.
func (registry *Registry) Get(urlKey string) (*User, error) {
	if registry == nil {
		return nil, fmt.Errorf("can't pop from nil registry")
	}
	if urlKey == "" {
		return nil, fmt.Errorf("can't pop unless provided a key")
	}

	registry.Mu.RLock()
	defer registry.Mu.RUnlock()

	if _, ok := registry.Users[urlKey]; !ok {
		return nil, fmt.Errorf("provided url key doesn't exist in registry")
	}

	registry.Users[urlKey].Mu.RLock()
	userGot := registry.Users[urlKey]
	registry.Users[urlKey].Mu.RUnlock()

	return userGot, nil
}

// DelUser removes a user and all associated data from
// the Registry.
func (registry *Registry) DelUser(urlKey string) error {

	if registry == nil {
		return fmt.Errorf("can't delete user from empty registry")

	} else if urlKey == "" {
		return fmt.Errorf("can't delete blank user")

	} else if !strings.HasPrefix(urlKey, "http") {
		return fmt.Errorf("invalid URL: %v", urlKey)
	}

	registry.Mu.Lock()
	defer registry.Mu.Unlock()

	if _, ok := registry.Users[urlKey]; !ok {
		return fmt.Errorf("can't delete user %v, user doesn't exist", urlKey)
	}

	delete(registry.Users, urlKey)

	return nil
}

// UpdateUser scrapes an existing user's remote twtxt.txt
// file. Any new statuses are added to the user's entry
// in the Registry. If the remote twtxt data's reported
// Content-Length does not differ from what is stored,
// an error is returned.
func (registry *Registry) UpdateUser(urlKey string) error {
	if urlKey == "" || !strings.HasPrefix(urlKey, "http") {
		return fmt.Errorf("invalid URL: %v", urlKey)
	}

	diff, err := registry.DiffTwtxt(urlKey)
	if err != nil {
		return err
	} else if !diff {
		return fmt.Errorf("no new statuses available for %v", urlKey)
	}

	out, isRemoteRegistry, err := GetTwtxt(urlKey, registry.HTTPClient)
	if err != nil {
		return err
	}

	if isRemoteRegistry {
		return fmt.Errorf("attempting to update registry URL - users should be updated individually")
	}

	registry.Mu.Lock()
	defer registry.Mu.Unlock()
	user := registry.Users[urlKey]

	user.Mu.Lock()
	defer user.Mu.Unlock()
	nick := user.Nick

	data, err := ParseUserTwtxt(out, nick, urlKey)
	if err != nil {
		return err
	}

	for i, e := range data {
		user.Status[i] = e
	}

	registry.Users[urlKey] = user

	return nil
}

// CrawlRemoteRegistry scrapes all nicknames and user URLs
// from a provided registry. The urlKey passed to this function
// must be in the form of https://registry.example.com/api/plain/users
func (registry *Registry) CrawlRemoteRegistry(urlKey string) error {
	if urlKey == "" || !strings.HasPrefix(urlKey, "http") {
		return fmt.Errorf("invalid URL: %v", urlKey)
	}

	out, isRemoteRegistry, err := GetTwtxt(urlKey, registry.HTTPClient)
	if err != nil {
		return err
	}

	if !isRemoteRegistry {
		return fmt.Errorf("can't add single user via call to CrawlRemoteRegistry")
	}

	users, err := ParseRegistryTwtxt(out)
	if err != nil {
		return err
	}

	// only add new users so we don't overwrite data
	// we already have (and lose statuses, etc)
	registry.Mu.Lock()
	defer registry.Mu.Unlock()
	for _, e := range users {
		if _, ok := registry.Users[e.URL]; !ok {
			registry.Users[e.URL] = e
		}
	}

	return nil
}

// GetUserStatuses returns a TimeMap containing single user's statuses
func (registry *Registry) GetUserStatuses(urlKey string) (TimeMap, error) {
	if registry == nil {
		return nil, fmt.Errorf("can't get statuses from an empty registry")
	} else if urlKey == "" || !strings.HasPrefix(urlKey, "http") {
		return nil, fmt.Errorf("invalid URL: %v", urlKey)
	}

	registry.Mu.RLock()
	defer registry.Mu.RUnlock()
	if _, ok := registry.Users[urlKey]; !ok {
		return nil, fmt.Errorf("can't retrieve statuses of nonexistent user")
	}

	registry.Users[urlKey].Mu.RLock()
	status := registry.Users[urlKey].Status
	registry.Users[urlKey].Mu.RUnlock()

	return status, nil
}

// GetStatuses returns a TimeMap containing all statuses
// from all users in the Registry.
func (registry *Registry) GetStatuses() (TimeMap, error) {
	if registry == nil {
		return nil, fmt.Errorf("can't get statuses from an empty registry")
	}

	statuses := NewTimeMap()

	registry.Mu.RLock()
	defer registry.Mu.RUnlock()

	for _, v := range registry.Users {
		v.Mu.RLock()
		if v.Status == nil || len(v.Status) == 0 {
			v.Mu.RUnlock()
			continue
		}
		for a, b := range v.Status {
			if _, ok := v.Status[a]; ok {
				statuses[a] = b
			}
		}
		v.Mu.RUnlock()
	}

	return statuses, nil
}