diff options
Diffstat (limited to 'registry/user.go')
-rw-r--r-- | registry/user.go | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/registry/user.go b/registry/user.go new file mode 100644 index 0000000..329b6e3 --- /dev/null +++ b/registry/user.go @@ -0,0 +1,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 +} |