Beeg refactor for database and users and auth
This commit is contained in:
parent
c5f2fa0699
commit
ef9544ff7d
|
@ -5,5 +5,7 @@
|
||||||
/willow
|
/willow
|
||||||
/*.csv
|
/*.csv
|
||||||
/data/
|
/data/
|
||||||
|
/*.sqlite
|
||||||
|
/config.toml
|
||||||
|
|
||||||
/.idea/
|
/.idea/
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.sr.ht/~amolith/willow/users"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createUser is a CLI that creates a new user with the specified username
|
||||||
|
func createUser(dbConn *sql.DB, username string) {
|
||||||
|
fmt.Println("Creating user", username)
|
||||||
|
|
||||||
|
fmt.Print("Enter password: ")
|
||||||
|
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading password:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Print("Confirm password: ")
|
||||||
|
passwordConfirmation, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading password confirmation:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if string(password) != string(passwordConfirmation) {
|
||||||
|
fmt.Println("Passwords do not match")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = users.Register(dbConn, username, string(password))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error creating user:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nUser", username, "created successfully")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteUser is a CLI that deletes a user with the specified username
|
||||||
|
func deleteUser(dbConn *sql.DB, username string) {
|
||||||
|
fmt.Println("Deleting user", username)
|
||||||
|
err := users.Delete(dbConn, username)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error deleting user:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("User %s deleted successfully\n", username)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// listUsers is a CLI that lists all users in the database
|
||||||
|
func listUsers(dbConn *sql.DB) {
|
||||||
|
fmt.Println("Listing all users")
|
||||||
|
|
||||||
|
dbUsers, err := users.GetUsers(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error retrieving users from the database:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dbUsers) == 0 {
|
||||||
|
fmt.Println("- No users found")
|
||||||
|
} else {
|
||||||
|
for _, u := range dbUsers {
|
||||||
|
fmt.Println("-", u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAuthorised is a CLI that checks whether the provided user/password
|
||||||
|
// combo is authorised.
|
||||||
|
func checkAuthorised(dbConn *sql.DB, username string) {
|
||||||
|
fmt.Printf("Checking whether password for user %s is correct\n", username)
|
||||||
|
|
||||||
|
fmt.Print("Enter password: ")
|
||||||
|
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading password:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
authorised, err := users.Authorised(dbConn, username, string(password))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error checking authorisation:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if authorised {
|
||||||
|
fmt.Println("User is authorised")
|
||||||
|
} else {
|
||||||
|
fmt.Println("User is not authorised")
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.sr.ht/~amolith/willow/db"
|
||||||
|
"git.sr.ht/~amolith/willow/project"
|
||||||
|
"git.sr.ht/~amolith/willow/ws"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Config struct {
|
||||||
|
Server server
|
||||||
|
CSVLocation string
|
||||||
|
DBConn string
|
||||||
|
// TODO: Make cache location configurable
|
||||||
|
// CacheLocation string
|
||||||
|
FetchInterval int
|
||||||
|
}
|
||||||
|
|
||||||
|
server struct {
|
||||||
|
Listen string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagConfig = flag.StringP("config", "c", "config.toml", "Path to config file")
|
||||||
|
flagAddUser = flag.StringP("add", "a", "", "Username of account to add")
|
||||||
|
flagDeleteUser = flag.StringP("deleteuser", "d", "", "Username of account to delete")
|
||||||
|
flagCheckAuthorised = flag.StringP("validatecredentials", "v", "", "Username of account to check")
|
||||||
|
flagListUsers = flag.BoolP("listusers", "l", false, "List all users")
|
||||||
|
config Config
|
||||||
|
req = make(chan struct{})
|
||||||
|
res = make(chan []project.Project)
|
||||||
|
manualRefresh = make(chan struct{})
|
||||||
|
)
|
||||||
|
|
||||||
|
//goland:noinspection GoUnusedFunction
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
err := checkConfig()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Opening database at", config.DBConn)
|
||||||
|
|
||||||
|
dbConn, err := db.Open(config.DBConn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error opening database:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Verifying database schema")
|
||||||
|
err = db.VerifySchema(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error verifying database schema:", err)
|
||||||
|
fmt.Println("Attempting to load schema")
|
||||||
|
err = db.LoadSchema(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error loading schema:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("Database schema verified")
|
||||||
|
|
||||||
|
if len(*flagAddUser) > 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
|
||||||
|
createUser(dbConn, *flagAddUser)
|
||||||
|
os.Exit(0)
|
||||||
|
} else if len(*flagAddUser) == 0 && len(*flagDeleteUser) > 0 && !*flagListUsers && len(*flagCheckAuthorised) == 0 {
|
||||||
|
deleteUser(dbConn, *flagDeleteUser)
|
||||||
|
os.Exit(0)
|
||||||
|
} else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && *flagListUsers && len(*flagCheckAuthorised) == 0 {
|
||||||
|
listUsers(dbConn)
|
||||||
|
os.Exit(0)
|
||||||
|
} else if len(*flagAddUser) == 0 && len(*flagDeleteUser) == 0 && !*flagListUsers && len(*flagCheckAuthorised) > 0 {
|
||||||
|
checkAuthorised(dbConn, *flagCheckAuthorised)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Starting refresh loop")
|
||||||
|
go project.RefreshLoop(dbConn, config.FetchInterval, &manualRefresh, &req, &res)
|
||||||
|
|
||||||
|
var mutex sync.Mutex
|
||||||
|
|
||||||
|
wsHandler := ws.Handler{
|
||||||
|
DbConn: dbConn,
|
||||||
|
Mutex: &mutex,
|
||||||
|
Req: &req,
|
||||||
|
Res: &res,
|
||||||
|
ManualRefresh: &manualRefresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", wsHandler.RootHandler)
|
||||||
|
mux.HandleFunc("/static", ws.StaticHandler)
|
||||||
|
mux.HandleFunc("/new", wsHandler.NewHandler)
|
||||||
|
mux.HandleFunc("/login", wsHandler.LoginHandler)
|
||||||
|
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: config.Server.Listen,
|
||||||
|
Handler: mux,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Starting web server on", config.Server.Listen)
|
||||||
|
if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) {
|
||||||
|
fmt.Println("Web server closed")
|
||||||
|
os.Exit(0)
|
||||||
|
} else {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfig() error {
|
||||||
|
file, err := os.Open(*flagConfig)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
file, err = os.Create(*flagConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = file.WriteString(`# Path to SQLite database
|
||||||
|
DBConn = "willow.sqlite"
|
||||||
|
# How often to fetch new releases in seconds
|
||||||
|
FetchInterval = 3600
|
||||||
|
|
||||||
|
[Server]
|
||||||
|
# Address to listen on
|
||||||
|
Listen = "127.0.0.1:1313"
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Config file created at", *flagConfig)
|
||||||
|
fmt.Println("Please edit it and restart the server")
|
||||||
|
os.Exit(0)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = toml.DecodeFile(*flagConfig, &config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.FetchInterval < 10 {
|
||||||
|
fmt.Println("Fetch interval is set to", config.FetchInterval, "seconds, but the minimum is 10, using 10")
|
||||||
|
config.FetchInterval = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Server.Listen == "" {
|
||||||
|
fmt.Println("No listen address specified, using 127.0.0.1:1313")
|
||||||
|
config.Server.Listen = "127.0.0.1:1313"
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.DBConn == "" {
|
||||||
|
fmt.Println("No SQLite path specified, using \"willow.sqlite\"")
|
||||||
|
config.DBConn = "willow.sqlite"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Embed the schema into the binary
|
||||||
|
//
|
||||||
|
//go:embed sql
|
||||||
|
var embeddedSQL embed.FS
|
||||||
|
|
||||||
|
// Open opens a connection to the SQLite database
|
||||||
|
func Open(dbPath string) (*sql.DB, error) {
|
||||||
|
return sql.Open("sqlite", dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifySchema(dbConn *sql.DB) error {
|
||||||
|
tables := []string{
|
||||||
|
"users",
|
||||||
|
"sessions",
|
||||||
|
"projects",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
name := ""
|
||||||
|
err := dbConn.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSchema loads the schema into the database
|
||||||
|
func LoadSchema(dbConn *sql.DB) error {
|
||||||
|
schema, err := embeddedSQL.ReadFile("sql/schema.sql")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbConn.Exec(string(schema))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
// CreateProject adds a project to the database
|
||||||
|
func CreateProject(db *sql.DB, url, name, forge, running string) error {
|
||||||
|
_, err := db.Exec("INSERT INTO projects (url, name, forge, version) VALUES (?, ?, ?, ?)", url, name, forge, running)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProject deletes a project from the database
|
||||||
|
func DeleteProject(db *sql.DB, url string) error {
|
||||||
|
_, err := db.Exec("DELETE FROM projects WHERE url = ?", url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = db.Exec("DELETE FROM releases WHERE project_url = ?", url)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject returns a project from the database
|
||||||
|
func GetProject(db *sql.DB, url string) (map[string]string, error) {
|
||||||
|
var name, forge, version string
|
||||||
|
err := db.QueryRow("SELECT name, forge, version FROM projects WHERE url = ?", url).Scan(&name, &forge, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
project := map[string]string{
|
||||||
|
"name": name,
|
||||||
|
"url": url,
|
||||||
|
"forge": forge,
|
||||||
|
"version": version,
|
||||||
|
}
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProject updates an existing project in the database
|
||||||
|
func UpdateProject(db *sql.DB, url, name, forge, running string) error {
|
||||||
|
_, err := db.Exec("UPDATE projects SET name=?, forge=?, version=? WHERE url=?", name, forge, running, url)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpsertProject adds or updates a project in the database
|
||||||
|
func UpsertProject(db *sql.DB, url, name, forge, running string) error {
|
||||||
|
_, err := db.Exec(`INSERT INTO projects (url, name, forge, version)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(url) DO
|
||||||
|
UPDATE SET
|
||||||
|
name = excluded.name,
|
||||||
|
forge = excluded.forge,
|
||||||
|
version = excluded.version;`, url, name, forge, running)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjects returns a list of all projects in the database
|
||||||
|
func GetProjects(db *sql.DB) ([]map[string]string, error) {
|
||||||
|
rows, err := db.Query("SELECT name, url, forge, version FROM projects")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var projects []map[string]string
|
||||||
|
for rows.Next() {
|
||||||
|
var name, url, forge, version string
|
||||||
|
err = rows.Scan(&name, &url, &forge, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
project := map[string]string{
|
||||||
|
"name": name,
|
||||||
|
"url": url,
|
||||||
|
"forge": forge,
|
||||||
|
"version": version,
|
||||||
|
}
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
// AddRelease adds a release for a project with a given URL to the database
|
||||||
|
|
||||||
|
// DeleteRelease deletes a release for a project with a given URL from the database
|
||||||
|
|
||||||
|
// UpdateRelease updates a release for a project with a given URL in the database
|
||||||
|
|
||||||
|
// UpsertRelease adds or updates a release for a project with a given URL in the
|
||||||
|
// database
|
||||||
|
func UpsertRelease(db *sql.DB, projectURL, releaseURL, tag, content, date string) error {
|
||||||
|
_, err := db.Exec(`INSERT INTO releases (project_url, release_url, tag, content, date)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(release_url) DO
|
||||||
|
UPDATE SET
|
||||||
|
release_url = excluded.release_url,
|
||||||
|
content = excluded.content,
|
||||||
|
tag = excluded.tag,
|
||||||
|
content = excluded.content,
|
||||||
|
date = excluded.date;`, projectURL, releaseURL, tag, content, date)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRelease returns a release for a project with a given URL from the database
|
||||||
|
|
||||||
|
// GetReleases returns all releases for a project with a given URL from the database
|
||||||
|
func GetReleases(db *sql.DB, projectURL string) ([]map[string]string, error) {
|
||||||
|
rows, err := db.Query(`SELECT project_url, release_url, tag, content, date FROM releases WHERE project_url = ?`, projectURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
releases := make([]map[string]string, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
projectURL string
|
||||||
|
releaseURL string
|
||||||
|
tag string
|
||||||
|
content string
|
||||||
|
date string
|
||||||
|
)
|
||||||
|
err := rows.Scan(&projectURL, &releaseURL, &tag, &content, &date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
releases = append(releases, map[string]string{
|
||||||
|
"projectURL": projectURL,
|
||||||
|
"releaseURL": releaseURL,
|
||||||
|
"tag": tag,
|
||||||
|
"content": content,
|
||||||
|
"date": date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return releases, nil
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
--
|
||||||
|
-- SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
-- Create table of users with username, password hash, salt, and creation
|
||||||
|
-- timestamp
|
||||||
|
CREATE TABLE users (
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
hash VARCHAR(255) NOT NULL,
|
||||||
|
salt VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (username)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create table of sessions with session GUID, username, and timestamp of when
|
||||||
|
-- the session was created
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
token VARCHAR(255) NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
expires TIMESTAMP NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (token)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create table of tracked projects with URL, name, forge, running version, and
|
||||||
|
-- timestamp of when the project was added
|
||||||
|
CREATE TABLE projects (
|
||||||
|
url VARCHAR(255) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
forge VARCHAR(255) NOT NULL,
|
||||||
|
version VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (url)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create table of project releases with the project URL and the release tags,
|
||||||
|
-- contents, URLs, and dates
|
||||||
|
CREATE TABLE releases (
|
||||||
|
project_url VARCHAR(255) NOT NULL,
|
||||||
|
release_url VARCHAR(255) NOT NULL,
|
||||||
|
tag VARCHAR(255) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (release_url)
|
||||||
|
);
|
|
@ -0,0 +1,84 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteUser deletes specific user from the database and returns an error if it
|
||||||
|
// fails
|
||||||
|
func DeleteUser(db *sql.DB, user string) error {
|
||||||
|
_, err := db.Exec("DELETE FROM users WHERE username = ?", user)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user in the database and returns an error if it fails
|
||||||
|
func CreateUser(db *sql.DB, username, hash, salt string) error {
|
||||||
|
_, err := db.Exec("INSERT INTO users (username, hash, salt) VALUES (?, ?, ?)", username, hash, salt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a user's hash and salt from the database as strings and
|
||||||
|
// returns an error if it fails
|
||||||
|
func GetUser(db *sql.DB, username string) (string, string, error) {
|
||||||
|
var hash, salt string
|
||||||
|
err := db.QueryRow("SELECT hash, salt FROM users WHERE username = ?", username).Scan(&hash, &salt)
|
||||||
|
return hash, salt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsers returns a list of all users in the database as a slice of strings
|
||||||
|
// and returns an error if it fails
|
||||||
|
func GetUsers(db *sql.DB) ([]string, error) {
|
||||||
|
rows, err := db.Query("SELECT username FROM users")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var users []string
|
||||||
|
for rows.Next() {
|
||||||
|
var user string
|
||||||
|
err = rows.Scan(&user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSession accepts a session ID and returns the username associated with it
|
||||||
|
// and an error
|
||||||
|
func GetSession(db *sql.DB, session string) (string, time.Time, error) {
|
||||||
|
var username string
|
||||||
|
var expiresString string
|
||||||
|
err := db.QueryRow("SELECT username, expires FROM sessions WHERE token = ?", session).Scan(&username, &expiresString)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expires, err := time.Parse(time.RFC3339, expiresString)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, err
|
||||||
|
}
|
||||||
|
return username, expires, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateSession invalidates a session by setting the expiration date to the
|
||||||
|
// provided time.
|
||||||
|
func InvalidateSession(db *sql.DB, session string, expiry time.Time) error {
|
||||||
|
_, err := db.Exec("UPDATE sessions SET expires = ? WHERE token = ?", expiry.Format(time.RFC3339), session)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSession creates a new session in the database and returns an error if
|
||||||
|
// it fails
|
||||||
|
func CreateSession(db *sql.DB, username, token string, expiry time.Time) error {
|
||||||
|
_, err := db.Exec("INSERT INTO sessions (token, username, expires) VALUES (?, ?, ?)", token, username, expiry.Format(time.RFC3339))
|
||||||
|
return err
|
||||||
|
}
|
|
@ -2,19 +2,36 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package main
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
Tag string
|
||||||
|
Content string
|
||||||
|
URL string
|
||||||
|
Date time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bmUGC = bluemonday.UGCPolicy()
|
||||||
|
bmStrict = bluemonday.StrictPolicy()
|
||||||
|
)
|
||||||
|
|
||||||
// listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH.
|
// listRemoteTags lists all tags in a remote repository, whether HTTP(S) or SSH.
|
||||||
// func listRemoteTags(url string) (tags []string, err error) {
|
// func listRemoteTags(url string) (tags []string, err error) {
|
||||||
// // TODO: Implement listRemoteTags
|
// // TODO: Implement listRemoteTags
|
||||||
|
@ -22,37 +39,51 @@ import (
|
||||||
// return nil, nil
|
// return nil, nil
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// fetchReleases fetches all releases in a remote repository, whether HTTP(S) or SSH.
|
// GetReleases fetches all releases in a remote repository, whether HTTP(S) or
|
||||||
func getGitReleases(p project) (project, error) {
|
// SSH.
|
||||||
r, err := minimalClone(p.URL)
|
func GetReleases(gitURI, forge string) ([]Release, error) {
|
||||||
|
r, err := minimalClone(gitURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tagRefs, err := r.Tags()
|
tagRefs, err := r.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedURI, err := url.Parse(gitURI)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error parsing URI: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpURI string
|
||||||
|
if parsedURI.Scheme != "" {
|
||||||
|
httpURI = parsedURI.Host + parsedURI.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
releases := make([]Release, 0)
|
||||||
|
|
||||||
err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error {
|
err = tagRefs.ForEach(func(tagRef *plumbing.Reference) error {
|
||||||
obj, err := r.TagObject(tagRef.Hash())
|
obj, err := r.TagObject(tagRef.Hash())
|
||||||
switch err {
|
switch {
|
||||||
case plumbing.ErrObjectNotFound:
|
case errors.Is(err, plumbing.ErrObjectNotFound):
|
||||||
// This is a lightweight tag, not an annotated tag, skip it
|
// This is a lightweight tag, not an annotated tag, skip it
|
||||||
return nil
|
return nil
|
||||||
case nil:
|
case err == nil:
|
||||||
url := ""
|
tagURL := ""
|
||||||
tagName := bmStrict.Sanitize(tagRef.Name().Short())
|
tagName := bmStrict.Sanitize(tagRef.Name().Short())
|
||||||
switch p.Forge {
|
switch forge {
|
||||||
case "sourcehut":
|
case "sourcehut":
|
||||||
url = p.URL + "/refs/" + tagName
|
tagURL = "https://" + httpURI + "/refs/" + tagName
|
||||||
case "gitlab":
|
case "gitlab":
|
||||||
url = p.URL + "/-/releases/" + tagName
|
tagURL = "https://" + httpURI + "/-/releases/" + tagName
|
||||||
default:
|
default:
|
||||||
url = ""
|
tagURL = ""
|
||||||
}
|
}
|
||||||
p.Releases = append(p.Releases, release{
|
releases = append(releases, Release{
|
||||||
Tag: tagName,
|
Tag: tagName,
|
||||||
Content: bmUGC.Sanitize(obj.Message),
|
Content: bmUGC.Sanitize(obj.Message),
|
||||||
URL: url,
|
URL: tagURL,
|
||||||
Date: obj.Tagger.When,
|
Date: obj.Tagger.When,
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
|
@ -61,12 +92,12 @@ func getGitReleases(p project) (project, error) {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) })
|
sort.Slice(releases, func(i, j int) bool { return releases[i].Date.After(releases[j].Date) })
|
||||||
|
|
||||||
return p, nil
|
return releases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// minimalClone clones a repository with a depth of 1 and no checkout.
|
// minimalClone clones a repository with a depth of 1 and no checkout.
|
||||||
|
@ -86,7 +117,7 @@ func minimalClone(url string) (r *git.Repository, err error) {
|
||||||
Depth: 1,
|
Depth: 1,
|
||||||
Tags: git.AllTags,
|
Tags: git.AllTags,
|
||||||
})
|
})
|
||||||
if err == git.NoErrAlreadyUpToDate {
|
if errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
return r, err
|
return r, err
|
||||||
|
@ -101,8 +132,8 @@ func minimalClone(url string) (r *git.Repository, err error) {
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeRepo removes a repository from the local filesystem.
|
// RemoveRepo removes a repository from the local filesystem.
|
||||||
func removeRepo(url string) (err error) {
|
func RemoveRepo(url string) (err error) {
|
||||||
path, err := stringifyRepo(url)
|
path, err := stringifyRepo(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
39
go.mod
39
go.mod
|
@ -8,41 +8,58 @@ go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/go-git/go-git/v5 v5.8.0
|
github.com/go-git/go-git/v5 v5.9.0
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25
|
github.com/microcosm-cc/bluemonday v1.0.25
|
||||||
github.com/mmcdole/gofeed v1.2.1
|
github.com/mmcdole/gofeed v1.2.1
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
|
golang.org/x/crypto v0.14.0
|
||||||
|
golang.org/x/term v0.13.0
|
||||||
|
modernc.org/sqlite v1.26.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
dario.cat/mergo v1.0.0 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
github.com/PuerkitoBio/goquery v1.8.1 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
github.com/imdario/mergo v0.3.16 // indirect
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/mmcdole/goxpp v1.1.0 // indirect
|
github.com/mmcdole/goxpp v1.1.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sergi/go-diff v1.3.1 // indirect
|
github.com/sergi/go-diff v1.3.1 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.1 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
golang.org/x/mod v0.13.0 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/net v0.16.0 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/tools v0.13.0 // indirect
|
||||||
golang.org/x/tools v0.11.0 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
|
modernc.org/cc/v3 v3.40.0 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||||
|
modernc.org/libc v1.24.1 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.6.0 // indirect
|
||||||
|
modernc.org/opt v0.1.3 // indirect
|
||||||
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
|
modernc.org/token v1.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
98
go.sum
98
go.sum
|
@ -1,10 +1,12 @@
|
||||||
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs=
|
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||||
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
|
||||||
|
@ -19,44 +21,51 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3
|
||||||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||||
|
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
|
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
|
||||||
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
|
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
|
||||||
github.com/go-git/go-git/v5 v5.8.0 h1:Rc543s6Tyq+YcyPwZRvU4jzZGM8rB/wWu94TnTIYALQ=
|
github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY=
|
||||||
github.com/go-git/go-git/v5 v5.8.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8=
|
github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
|
||||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
||||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||||
|
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||||
github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
|
github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
|
||||||
|
@ -68,18 +77,22 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
|
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
|
||||||
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
|
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
@ -95,12 +108,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
@ -111,8 +124,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
|
||||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
@ -126,20 +139,22 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -148,22 +163,45 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
|
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||||
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||||
|
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||||
|
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||||
|
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||||
|
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||||
|
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||||
|
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||||
|
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||||
|
modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
|
||||||
|
modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
|
||||||
|
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||||
|
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||||
|
modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
|
||||||
|
modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||||
|
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||||
|
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||||
|
modernc.org/sqlite v1.26.0 h1:SocQdLRSYlA8W99V8YH0NES75thx19d9sB/aFc4R8Lw=
|
||||||
|
modernc.org/sqlite v1.26.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU=
|
||||||
|
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||||
|
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||||
|
modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
|
||||||
|
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||||
|
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
|
||||||
|
|
35
justfile
35
justfile
|
@ -2,25 +2,40 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: CC0-1.0
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
default: reuse lint test staticcheck
|
default: fmt lint staticcheck test vuln reuse
|
||||||
|
|
||||||
reuse:
|
fmt:
|
||||||
reuse lint
|
# Formatting all Go source code
|
||||||
|
go install mvdan.cc/gofumpt@latest
|
||||||
|
gofumpt -l -w .
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
# Linting Go code
|
# Linting Go source code
|
||||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
test:
|
|
||||||
# Running tests
|
|
||||||
go test -v ./...
|
|
||||||
|
|
||||||
staticcheck:
|
staticcheck:
|
||||||
# Performing static analysis
|
# Performing static analysis
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
staticcheck ./...
|
staticcheck ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
# Running tests
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
vuln:
|
||||||
|
# Checking for vulnerabilities
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
|
|
||||||
|
reuse:
|
||||||
|
# Linting licenses and copyright headers
|
||||||
|
reuse lint
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
# Cleaning up
|
# Cleaning up
|
||||||
rm -rf willow
|
rm -rf willow out/
|
||||||
|
|
||||||
|
clean-all:
|
||||||
|
# Removing build artifacts, willow.sqlite, config.toml, and data/ directory
|
||||||
|
|
||||||
|
rm -rf willow out willow.sqlite config.toml data
|
||||||
|
|
228
main.go
228
main.go
|
@ -1,228 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/csv"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
|
||||||
flag "github.com/spf13/pflag"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Model struct {
|
|
||||||
Projects []project
|
|
||||||
}
|
|
||||||
|
|
||||||
project struct {
|
|
||||||
URL string
|
|
||||||
Name string
|
|
||||||
Forge string
|
|
||||||
Running string
|
|
||||||
Releases []release
|
|
||||||
}
|
|
||||||
|
|
||||||
release struct {
|
|
||||||
Tag string
|
|
||||||
Content string
|
|
||||||
URL string
|
|
||||||
Date time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
Config struct {
|
|
||||||
Server server
|
|
||||||
CSVLocation string
|
|
||||||
// TODO: Make cache location configurable
|
|
||||||
// CacheLocation string
|
|
||||||
FetchInterval int
|
|
||||||
}
|
|
||||||
|
|
||||||
server struct {
|
|
||||||
Listen string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagConfig *string = flag.StringP("config", "c", "config.toml", "Path to config file")
|
|
||||||
config Config
|
|
||||||
req = make(chan struct{})
|
|
||||||
manualRefresh = make(chan struct{})
|
|
||||||
res = make(chan []project)
|
|
||||||
m = Model{
|
|
||||||
Projects: []project{},
|
|
||||||
}
|
|
||||||
bmUGC = bluemonday.UGCPolicy()
|
|
||||||
bmStrict = bluemonday.StrictPolicy()
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
err := checkConfig()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = checkCSV()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := csv.NewReader(strings.NewReader(config.CSVLocation))
|
|
||||||
|
|
||||||
records, err := reader.ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Projects = []project{}
|
|
||||||
if len(records) > 0 {
|
|
||||||
for i, record := range records {
|
|
||||||
if i == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.Projects = append(m.Projects, project{
|
|
||||||
URL: record[0],
|
|
||||||
Name: record[1],
|
|
||||||
Forge: record[2],
|
|
||||||
Running: record[3],
|
|
||||||
Releases: []release{},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go refreshLoop(manualRefresh, req, res)
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: config.Server.Listen,
|
|
||||||
Handler: mux,
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.HandleFunc("/", rootHandler)
|
|
||||||
mux.HandleFunc("/static", staticHandler)
|
|
||||||
mux.HandleFunc("/new", newHandler)
|
|
||||||
|
|
||||||
if err := httpServer.ListenAndServe(); errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Println("Web server closed")
|
|
||||||
} else {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshLoop(manualRefresh, req chan struct{}, res chan []project) {
|
|
||||||
ticker := time.NewTicker(time.Second * 3600)
|
|
||||||
|
|
||||||
fetch := func() []project {
|
|
||||||
projects := make([]project, len(m.Projects))
|
|
||||||
copy(projects, m.Projects)
|
|
||||||
for i, project := range projects {
|
|
||||||
project, err := getReleases(project)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
projects[i] = project
|
|
||||||
}
|
|
||||||
sort.Slice(projects, func(i, j int) bool { return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name) })
|
|
||||||
return projects
|
|
||||||
}
|
|
||||||
|
|
||||||
projects := fetch()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
projects = fetch()
|
|
||||||
case <-manualRefresh:
|
|
||||||
ticker.Reset(time.Second * 3600)
|
|
||||||
projects = fetch()
|
|
||||||
case <-req:
|
|
||||||
projectsCopy := make([]project, len(projects))
|
|
||||||
copy(projectsCopy, projects)
|
|
||||||
res <- projectsCopy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkConfig() error {
|
|
||||||
file, err := os.Open(*flagConfig)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
file, err = os.Create(*flagConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = file.WriteString("# Location of the CSV file containing the projects\nCSVLocation = \"projects.csv\"\n# How often to fetch new releases in seconds\nFetchInterval = 3600\n\n[Server]\n# Address to listen on\nListen = \"127.0.0.1:1313\"\n")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Config file created at", *flagConfig)
|
|
||||||
fmt.Println("Please edit it and restart the server")
|
|
||||||
os.Exit(0)
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = toml.DecodeFile(*flagConfig, &config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.CSVLocation == "" {
|
|
||||||
fmt.Println("No CSV location specified, using projects.csv")
|
|
||||||
config.CSVLocation = "projects.csv"
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.FetchInterval < 10 {
|
|
||||||
fmt.Println("Fetch interval is set to", config.FetchInterval, "seconds, but the minimum is 10, using 10")
|
|
||||||
config.FetchInterval = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.Server.Listen == "" {
|
|
||||||
fmt.Println("No listen address specified, using 127.0.0.1:1313")
|
|
||||||
config.Server.Listen = "127.0.0.1:1313"
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCSV() error {
|
|
||||||
file, err := os.Open(config.CSVLocation)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
file, err = os.Create(config.CSVLocation)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = file.WriteString("url,name,forge,running\nhttps://git.sr.ht/~amolith/earl,earl,sourcehut,v0.0.1-rc0\n")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package project
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~amolith/willow/db"
|
||||||
|
"git.sr.ht/~amolith/willow/git"
|
||||||
|
"git.sr.ht/~amolith/willow/rss"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
URL string
|
||||||
|
Name string
|
||||||
|
Forge string
|
||||||
|
Running string
|
||||||
|
Releases []Release
|
||||||
|
}
|
||||||
|
|
||||||
|
type Release struct {
|
||||||
|
URL string
|
||||||
|
Tag string
|
||||||
|
Content string
|
||||||
|
Date time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReleases returns a list of all releases for a project from the database
|
||||||
|
func GetReleases(dbConn *sql.DB, proj Project) (Project, error) {
|
||||||
|
ret, err := db.GetReleases(dbConn, proj.URL)
|
||||||
|
if err != nil {
|
||||||
|
return proj, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return fetchReleases(proj)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range ret {
|
||||||
|
proj.Releases = append(proj.Releases, Release{
|
||||||
|
Tag: row["tag"],
|
||||||
|
Content: row["content"],
|
||||||
|
URL: row["release_url"],
|
||||||
|
Date: time.Time{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sort.Slice(proj.Releases, func(i, j int) bool {
|
||||||
|
return proj.Releases[i].Date.After(proj.Releases[j].Date)
|
||||||
|
})
|
||||||
|
return proj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchReleases fetches releases from a project's forge given its URI
|
||||||
|
func fetchReleases(p Project) (Project, error) {
|
||||||
|
var err error
|
||||||
|
switch p.Forge {
|
||||||
|
case "github", "gitea", "forgejo":
|
||||||
|
rssReleases, err := rss.GetReleases(p.URL)
|
||||||
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
for _, release := range rssReleases {
|
||||||
|
p.Releases = append(p.Releases, Release{
|
||||||
|
Tag: release.Tag,
|
||||||
|
Content: release.Content,
|
||||||
|
URL: release.URL,
|
||||||
|
Date: release.Date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
gitReleases, err := git.GetReleases(p.URL, p.Forge)
|
||||||
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
for _, release := range gitReleases {
|
||||||
|
p.Releases = append(p.Releases, Release{
|
||||||
|
Tag: release.Tag,
|
||||||
|
Content: release.Content,
|
||||||
|
URL: release.URL,
|
||||||
|
Date: release.Date,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(p.Releases, func(i, j int) bool {
|
||||||
|
return p.Releases[i].Date.After(p.Releases[j].Date)
|
||||||
|
})
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) {
|
||||||
|
err := db.UpsertProject(dbConn, url, name, forge, release)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error upserting project:", err)
|
||||||
|
}
|
||||||
|
*manualRefresh <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) {
|
||||||
|
err := db.DeleteProject(dbConn, url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error deleting project:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*manualRefresh <- struct{}{}
|
||||||
|
|
||||||
|
err = git.RemoveRepo(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RefreshLoop(dbConn *sql.DB, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
|
||||||
|
ticker := time.NewTicker(time.Second * time.Duration(interval))
|
||||||
|
|
||||||
|
fetch := func() []Project {
|
||||||
|
projectsList, err := GetProjects(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error getting projects:", err)
|
||||||
|
}
|
||||||
|
for i, p := range projectsList {
|
||||||
|
p, err := fetchReleases(p)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
projectsList[i] = p
|
||||||
|
}
|
||||||
|
sort.Slice(projectsList, func(i, j int) bool {
|
||||||
|
return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
|
||||||
|
})
|
||||||
|
for i := range projectsList {
|
||||||
|
for j := range projectsList[i].Releases {
|
||||||
|
err = db.UpsertRelease(dbConn, projectsList[i].URL, projectsList[i].Releases[j].URL, projectsList[i].Releases[j].Tag, projectsList[i].Releases[j].Content, projectsList[i].Releases[j].Date.Format("2006-01-02 15:04:05"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error upserting release:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return projectsList
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := fetch()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
projects = fetch()
|
||||||
|
case <-*manualRefresh:
|
||||||
|
ticker.Reset(time.Second * 3600)
|
||||||
|
projects = fetch()
|
||||||
|
case <-*req:
|
||||||
|
projectsCopy := make([]Project, len(projects))
|
||||||
|
copy(projectsCopy, projects)
|
||||||
|
*res <- projectsCopy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProject returns a project from the database
|
||||||
|
func GetProject(dbConn *sql.DB, url string) (Project, error) {
|
||||||
|
var p Project
|
||||||
|
projectDB, err := db.GetProject(dbConn, url)
|
||||||
|
if err != nil {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
p = Project{
|
||||||
|
URL: projectDB["url"],
|
||||||
|
Name: projectDB["name"],
|
||||||
|
Forge: projectDB["forge"],
|
||||||
|
Running: projectDB["version"],
|
||||||
|
}
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjects returns a list of all projects from the database
|
||||||
|
func GetProjects(dbConn *sql.DB) ([]Project, error) {
|
||||||
|
projectsDB, err := db.GetProjects(dbConn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projects := make([]Project, len(projectsDB))
|
||||||
|
for i, p := range projectsDB {
|
||||||
|
projects[i] = Project{
|
||||||
|
URL: p["url"],
|
||||||
|
Name: p["name"],
|
||||||
|
Forge: p["forge"],
|
||||||
|
Running: p["version"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
85
releases.go
85
releases.go
|
@ -1,85 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/csv"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getReleases(p project) (project, error) {
|
|
||||||
var err error
|
|
||||||
switch p.Forge {
|
|
||||||
case "github", "gitea", "forgejo":
|
|
||||||
p, err = getRSSReleases(p)
|
|
||||||
// case "gitlab":
|
|
||||||
// // TODO: maybe use GitLab's API?
|
|
||||||
default:
|
|
||||||
p, err = getGitReleases(p)
|
|
||||||
}
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func track(name, url, forge, release string) {
|
|
||||||
projectExists := false
|
|
||||||
for i := range m.Projects {
|
|
||||||
if m.Projects[i].URL == url {
|
|
||||||
projectExists = true
|
|
||||||
m.Projects[i].Running = release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !projectExists {
|
|
||||||
m.Projects = append(m.Projects, project{
|
|
||||||
URL: url,
|
|
||||||
Name: name,
|
|
||||||
Forge: forge,
|
|
||||||
Running: release,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
manualRefresh <- struct{}{}
|
|
||||||
|
|
||||||
writeCSV()
|
|
||||||
}
|
|
||||||
|
|
||||||
func untrack(url string) {
|
|
||||||
for i := range m.Projects {
|
|
||||||
if m.Projects[i].URL == url {
|
|
||||||
m.Projects = append(m.Projects[:i], m.Projects[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
manualRefresh <- struct{}{}
|
|
||||||
|
|
||||||
writeCSV()
|
|
||||||
err := removeRepo(url)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeCSV() {
|
|
||||||
file, err := os.OpenFile(config.CSVLocation, os.O_RDWR|os.O_CREATE, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
writer := csv.NewWriter(file)
|
|
||||||
|
|
||||||
if err := writer.Write([]string{"url", "name", "forge", "running"}); err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
for _, project := range m.Projects {
|
|
||||||
if err := writer.Write([]string{project.URL, project.Name, project.Forge, project.Running}); err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Flush()
|
|
||||||
}
|
|
|
@ -2,24 +2,41 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package main
|
package rss
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
|
||||||
"github.com/mmcdole/gofeed"
|
"github.com/mmcdole/gofeed"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getRSSReleases(p project) (project, error) {
|
type Release struct {
|
||||||
fp := gofeed.NewParser()
|
Tag string
|
||||||
feed, err := fp.ParseURL(p.URL + "/releases.atom")
|
Content string
|
||||||
if err != nil {
|
URL string
|
||||||
fmt.Println(err)
|
Date time.Time
|
||||||
return p, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bmUGC = bluemonday.UGCPolicy()
|
||||||
|
bmStrict = bluemonday.StrictPolicy()
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetReleases(feedURL string) ([]Release, error) {
|
||||||
|
fp := gofeed.NewParser()
|
||||||
|
feed, err := fp.ParseURL(feedURL + "/releases.atom")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
releases := make([]Release, 0)
|
||||||
|
|
||||||
for _, item := range feed.Items {
|
for _, item := range feed.Items {
|
||||||
p.Releases = append(p.Releases, release{
|
releases = append(releases, Release{
|
||||||
Tag: bmStrict.Sanitize(item.Title),
|
Tag: bmStrict.Sanitize(item.Title),
|
||||||
Content: bmUGC.Sanitize(item.Content),
|
Content: bmUGC.Sanitize(item.Content),
|
||||||
URL: bmStrict.Sanitize(item.Link),
|
URL: bmStrict.Sanitize(item.Link),
|
||||||
|
@ -30,5 +47,5 @@ func getRSSReleases(p project) (project, error) {
|
||||||
// TODO: Doesn't seem to work?
|
// TODO: Doesn't seem to work?
|
||||||
// sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) })
|
// sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) })
|
||||||
|
|
||||||
return p, nil
|
return releases, nil
|
||||||
}
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package users
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.sr.ht/~amolith/willow/db"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// argonHash accepts two strings for the user's password and a random salt,
|
||||||
|
// hashes the password using the salt, and returns the hash as a base64-encoded
|
||||||
|
// string.
|
||||||
|
func argonHash(password, salt string) (string, error) {
|
||||||
|
decodedSalt, err := base64.StdEncoding.DecodeString(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(argon2.IDKey([]byte(password), decodedSalt, 2, 64*1024, 4, 64)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSalt generates a random salt and returns it as a base64-encoded
|
||||||
|
// string.
|
||||||
|
func generateSalt() (string, error) {
|
||||||
|
salt := make([]byte, 16)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(salt), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register accepts a username and password, hashes the password and stores the
|
||||||
|
// hash and salt in the database.
|
||||||
|
func Register(dbConn *sql.DB, username, password string) error {
|
||||||
|
salt, err := generateSalt()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := argonHash(password, salt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.CreateUser(dbConn, username, hash, salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a user from the database.
|
||||||
|
func Delete(dbConn *sql.DB, username string) error { return db.DeleteUser(dbConn, username) }
|
||||||
|
|
||||||
|
// Authorised accepts a username string, a token string, and returns true if the
|
||||||
|
// user is authorised, false if not, and an error if one is encountered.
|
||||||
|
func Authorised(dbConn *sql.DB, username, token string) (bool, error) {
|
||||||
|
dbHash, dbSalt, err := db.GetUser(dbConn, username)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
providedHash, err := argonHash(token, dbSalt)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbHash == providedHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSession accepts a session cookie string and returns the username
|
||||||
|
func GetSession(dbConn *sql.DB, session string) (string, time.Time, error) {
|
||||||
|
return db.GetSession(dbConn, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateSession invalidates a session by setting the expiration date to the
|
||||||
|
// current time.
|
||||||
|
func InvalidateSession(dbConn *sql.DB, session string) error {
|
||||||
|
return db.InvalidateSession(dbConn, session, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSession accepts a username and a token and creates a session in the
|
||||||
|
// database.
|
||||||
|
func CreateSession(dbConn *sql.DB, username, token string, expiry time.Time) error {
|
||||||
|
return db.CreateSession(dbConn, username, token, expiry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsers returns a list of all users in the database as a slice of strings.
|
||||||
|
func GetUsers(dbConn *sql.DB) ([]string, error) { return db.GetUsers(dbConn) }
|
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-GB">
|
||||||
|
<head>
|
||||||
|
<title>Willow</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="title" content="Willow">
|
||||||
|
<meta name="description" content="Willow">
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: auto auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Willow</h1>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="input">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username">
|
||||||
|
</div>
|
||||||
|
<div class="input">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password">
|
||||||
|
</div>
|
||||||
|
<input class="button" type="submit" formaction="/login" value="Login">
|
||||||
|
</form>
|
||||||
|
<p><a href="https://sr.ht/~amolith/willow">Source code</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,3 @@
|
||||||
|
SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
|
@ -2,31 +2,55 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package main
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"git.sr.ht/~amolith/willow/project"
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
DbConn *sql.DB
|
||||||
|
Mutex *sync.Mutex
|
||||||
|
Req *chan struct{}
|
||||||
|
ManualRefresh *chan struct{}
|
||||||
|
Res *chan []project.Project
|
||||||
|
}
|
||||||
|
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var fs embed.FS
|
var fs embed.FS
|
||||||
|
|
||||||
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
// bmUGC = bluemonday.UGCPolicy()
|
||||||
req <- struct{}{}
|
var bmStrict = bluemonday.StrictPolicy()
|
||||||
data := <-res
|
|
||||||
|
func (h Handler) RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.isAuthorised(r) {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*h.Req <- struct{}{}
|
||||||
|
data := <-*h.Res
|
||||||
tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
|
tmpl := template.Must(template.ParseFS(fs, "static/home.html"))
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHandler(w http.ResponseWriter, r *http.Request) {
|
func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.isAuthorised(r) {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
params := r.URL.Query()
|
params := r.URL.Query()
|
||||||
action := bmStrict.Sanitize(params.Get("action"))
|
action := bmStrict.Sanitize(params.Get("action"))
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
|
@ -36,7 +60,7 @@ func newHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
} else if action != "delete" {
|
} else if action != "delete" {
|
||||||
submittedURL := bmStrict.Sanitize(params.Get("submittedURL"))
|
submittedURL := bmStrict.Sanitize(params.Get("url"))
|
||||||
if submittedURL == "" {
|
if submittedURL == "" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
_, err := w.Write([]byte("No URL provided"))
|
_, err := w.Write([]byte("No URL provided"))
|
||||||
|
@ -62,22 +86,20 @@ func newHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
_, err := w.Write([]byte("No name provided"))
|
_, err := w.Write([]byte("No name provided"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proj := project{
|
proj := project.Project{
|
||||||
URL: submittedURL,
|
URL: submittedURL,
|
||||||
Name: name,
|
Name: name,
|
||||||
Forge: forge,
|
Forge: forge,
|
||||||
}
|
}
|
||||||
proj, err := getReleases(proj)
|
proj, err := project.GetReleases(h.DbConn, proj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
_, err := w.Write([]byte(fmt.Sprintf("Error getting releases: %s", err)))
|
_, err := w.Write([]byte(fmt.Sprintf("Error getting releases: %s", err)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tmpl := template.Must(template.ParseFS(fs, "static/select-release.html"))
|
tmpl := template.Must(template.ParseFS(fs, "static/select-release.html"))
|
||||||
|
@ -85,17 +107,16 @@ func newHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
} else if action == "delete" {
|
} else if action == "delete" {
|
||||||
submittedURL := params.Get("submittedURL")
|
submittedURL := params.Get("url")
|
||||||
if submittedURL == "" {
|
if submittedURL == "" {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
_, err := w.Write([]byte("No URL provided"))
|
_, err := w.Write([]byte("No URL provided"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
untrack(submittedURL)
|
project.Untrack(h.DbConn, h.ManualRefresh, submittedURL)
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +132,7 @@ func newHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
releaseValue := bmStrict.Sanitize(r.FormValue("release"))
|
releaseValue := bmStrict.Sanitize(r.FormValue("release"))
|
||||||
|
|
||||||
if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue != "" {
|
if nameValue != "" && urlValue != "" && forgeValue != "" && releaseValue != "" {
|
||||||
track(nameValue, urlValue, forgeValue, releaseValue)
|
project.Track(h.DbConn, h.ManualRefresh, nameValue, urlValue, forgeValue, releaseValue)
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -126,13 +147,21 @@ func newHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
_, err := w.Write([]byte("No data provided"))
|
_, err := w.Write([]byte("No data provided"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func staticHandler(writer http.ResponseWriter, request *http.Request) {
|
func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: do this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) isAuthorised(r *http.Request) bool {
|
||||||
|
// TODO: do this
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func StaticHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
resource := strings.TrimPrefix(request.URL.Path, "/")
|
resource := strings.TrimPrefix(request.URL.Path, "/")
|
||||||
// if path ends in .css, set content type to text/css
|
// if path ends in .css, set content type to text/css
|
||||||
if strings.HasSuffix(resource, ".css") {
|
if strings.HasSuffix(resource, ".css") {
|
Loading…
Reference in New Issue