about summary refs log blame commit diff stats
path: root/registry/user.go
blob: 329b6e3e2e2340d7f29c7217c408c3ae36c24311 (plain) (tree)













































































































































































































































































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