about summary refs log tree commit diff stats
path: root/registry/user.go
diff options
context:
space:
mode:
authorBen Morrison <ben@gbmor.dev>2020-06-20 02:27:31 -0400
committerBen Morrison <ben@gbmor.dev>2020-06-20 02:27:31 -0400
commit538e305925b9b04102ef0a4fb7cca19a6c116142 (patch)
tree8e68b426c40c3151f39da1be874d2938f31ab5f9 /registry/user.go
parent0a69c582ec8b88b1d3af70ef43c3eeb1b99f973d (diff)
downloadgetwtxt-538e305925b9b04102ef0a4fb7cca19a6c116142.tar.gz
updating module to live at sourcehut
Also moving the 'registry' library into this repo, rather
than maintaining them separately. It will still be decoupled,
just live in this repository.
Diffstat (limited to 'registry/user.go')
-rw-r--r--registry/user.go270
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
+}