/*
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
}