2023-03-12 15:00:57 +00:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-03-02 17:26:30 +00:00
2021-08-25 13:34:33 +00:00
package bundb
2021-03-02 17:26:30 +00:00
import (
"context"
2021-07-19 16:03:07 +00:00
"crypto/tls"
"crypto/x509"
2021-08-25 13:34:33 +00:00
"database/sql"
2021-07-19 16:03:07 +00:00
"encoding/pem"
2021-03-02 17:26:30 +00:00
"errors"
"fmt"
2021-07-19 16:03:07 +00:00
"os"
2021-09-20 16:20:21 +00:00
"runtime"
2023-01-17 12:29:44 +00:00
"strconv"
2021-03-03 17:12:02 +00:00
"strings"
2021-03-02 21:52:31 +00:00
"time"
2021-03-02 17:26:30 +00:00
2023-01-17 12:29:44 +00:00
"codeberg.org/gruf/go-bytesize"
2022-08-22 09:21:36 +00:00
"github.com/google/uuid"
2021-08-25 13:34:33 +00:00
"github.com/jackc/pgx/v4"
"github.com/jackc/pgx/v4/stdlib"
2021-04-01 18:46:45 +00:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2021-05-15 09:58:11 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2021-08-31 17:27:02 +00:00
"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations"
2021-05-08 12:25:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2021-06-13 16:42:28 +00:00
"github.com/superseriousbusiness/gotosocial/internal/id"
2022-07-19 08:47:55 +00:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2022-12-08 17:35:14 +00:00
"github.com/superseriousbusiness/gotosocial/internal/state"
2021-08-25 13:34:33 +00:00
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
2021-08-29 14:41:41 +00:00
"github.com/uptrace/bun/dialect/sqlitedialect"
2021-08-31 17:27:02 +00:00
"github.com/uptrace/bun/migrate"
2021-09-30 09:16:23 +00:00
2021-11-21 16:41:51 +00:00
"modernc.org/sqlite"
2021-08-25 13:34:33 +00:00
)
2021-11-22 07:46:19 +00:00
var registerTables = [ ] interface { } {
2022-09-26 09:56:01 +00:00
& gtsmodel . AccountToEmoji { } ,
2021-08-20 10:26:56 +00:00
& gtsmodel . StatusToEmoji { } ,
& gtsmodel . StatusToTag { } ,
}
2022-09-26 09:56:01 +00:00
// DBService satisfies the DB interface
type DBService struct {
2021-08-20 10:26:56 +00:00
db . Account
db . Admin
db . Basic
db . Domain
2022-05-20 08:34:36 +00:00
db . Emoji
2021-08-20 10:26:56 +00:00
db . Instance
db . Media
db . Mention
db . Notification
db . Relationship
2023-01-10 14:19:05 +00:00
db . Report
2021-08-25 13:34:33 +00:00
db . Session
2021-08-20 10:26:56 +00:00
db . Status
2023-03-20 18:10:08 +00:00
db . StatusBookmark
db . StatusFave
2021-08-20 10:26:56 +00:00
db . Timeline
2022-10-03 08:46:11 +00:00
db . User
2022-11-11 11:18:38 +00:00
db . Tombstone
2021-12-07 12:31:39 +00:00
conn * DBConn
2021-03-02 17:26:30 +00:00
}
2022-09-26 09:56:01 +00:00
// GetConn returns the underlying bun connection.
// Should only be used in testing + exceptional circumstance.
func ( dbService * DBService ) GetConn ( ) * DBConn {
return dbService . conn
}
2021-10-11 12:37:33 +00:00
func doMigration ( ctx context . Context , db * bun . DB ) error {
2021-08-31 17:27:02 +00:00
migrator := migrate . NewMigrator ( db , migrations . Migrations )
if err := migrator . Init ( ctx ) ; err != nil {
return err
}
group , err := migrator . Migrate ( ctx )
if err != nil {
2021-09-01 16:29:25 +00:00
if err . Error ( ) == "migrate: there are no any migrations" {
return nil
}
2021-08-31 17:27:02 +00:00
return err
}
if group . ID == 0 {
2023-02-17 11:02:29 +00:00
log . Info ( ctx , "there are no new migrations to run" )
2021-08-31 17:27:02 +00:00
return nil
}
2023-02-17 11:02:29 +00:00
log . Infof ( ctx , "MIGRATED DATABASE TO %s" , group )
2021-08-31 17:27:02 +00:00
return nil
}
2021-08-25 13:34:33 +00:00
// NewBunDBService returns a bunDB derived from the provided config, which implements the go-fed DB interface.
// Under the hood, it uses https://github.com/uptrace/bun to create and maintain a database connection.
2022-12-08 17:35:14 +00:00
func NewBunDBService ( ctx context . Context , state * state . State ) ( db . DB , error ) {
2021-08-29 14:41:41 +00:00
var conn * DBConn
2021-11-21 16:41:51 +00:00
var err error
2023-01-17 12:29:44 +00:00
t := strings . ToLower ( config . GetDbType ( ) )
2021-12-07 12:31:39 +00:00
2023-01-17 12:29:44 +00:00
switch t {
case "postgres" :
2021-12-07 12:31:39 +00:00
conn , err = pgConn ( ctx )
2021-08-25 13:34:33 +00:00
if err != nil {
2021-11-21 16:41:51 +00:00
return nil , err
2021-08-25 13:34:33 +00:00
}
2023-01-17 12:29:44 +00:00
case "sqlite" :
2021-12-07 12:31:39 +00:00
conn , err = sqliteConn ( ctx )
2021-08-29 14:41:41 +00:00
if err != nil {
2021-11-21 16:41:51 +00:00
return nil , err
2021-08-29 14:41:41 +00:00
}
2021-08-25 13:34:33 +00:00
default :
2023-01-17 12:29:44 +00:00
return nil , fmt . Errorf ( "database type %s not supported for bundb" , t )
2021-03-05 17:31:12 +00:00
}
2022-07-10 15:18:21 +00:00
// Add database query hook
conn . DB . AddQueryHook ( queryHook { } )
2021-09-11 11:19:06 +00:00
2023-01-17 12:29:44 +00:00
// execute sqlite pragmas *after* adding database hook;
// this allows the pragma queries to be logged
if t == "sqlite" {
if err := sqlitePragmas ( ctx , conn ) ; err != nil {
return nil , err
}
}
2021-11-21 16:41:51 +00:00
// table registration is needed for many-to-many, see:
// https://bun.uptrace.dev/orm/many-to-many-relation/
2021-08-25 13:34:33 +00:00
for _ , t := range registerTables {
conn . RegisterModel ( t )
2021-03-02 21:52:31 +00:00
}
2021-11-21 16:41:51 +00:00
// perform any pending database migrations: this includes
// the very first 'migration' on startup which just creates
// necessary tables
2021-10-11 12:37:33 +00:00
if err := doMigration ( ctx , conn . DB ) ; err != nil {
2021-08-31 17:27:02 +00:00
return nil , fmt . Errorf ( "db migration error: %s" , err )
}
2021-09-01 09:08:21 +00:00
2022-09-26 09:56:01 +00:00
ps := & DBService {
2022-12-08 17:35:14 +00:00
Account : & accountDB {
conn : conn ,
state : state ,
} ,
2021-08-20 10:26:56 +00:00
Admin : & adminDB {
2022-12-08 17:35:14 +00:00
conn : conn ,
state : state ,
2021-08-20 10:26:56 +00:00
} ,
Basic : & basicDB {
2021-12-07 12:31:39 +00:00
conn : conn ,
2021-08-20 10:26:56 +00:00
} ,
2022-12-08 17:35:14 +00:00
Domain : & domainDB {
conn : conn ,
state : state ,
} ,
Emoji : & emojiDB {
conn : conn ,
state : state ,
} ,
2021-08-20 10:26:56 +00:00
Instance : & instanceDB {
2021-12-07 12:31:39 +00:00
conn : conn ,
2021-08-20 10:26:56 +00:00
} ,
Media : & mediaDB {
2023-02-13 20:19:51 +00:00
conn : conn ,
state : state ,
2021-08-20 10:26:56 +00:00
} ,
2022-12-08 17:35:14 +00:00
Mention : & mentionDB {
conn : conn ,
state : state ,
} ,
Notification : & notificationDB {
conn : conn ,
state : state ,
} ,
Relationship : & relationshipDB {
conn : conn ,
state : state ,
} ,
2023-01-10 14:19:05 +00:00
Report : & reportDB {
conn : conn ,
state : state ,
} ,
2021-08-25 13:34:33 +00:00
Session : & sessionDB {
2021-12-07 12:31:39 +00:00
conn : conn ,
2021-08-20 10:26:56 +00:00
} ,
2022-12-08 17:35:14 +00:00
Status : & statusDB {
conn : conn ,
state : state ,
} ,
2023-03-20 18:10:08 +00:00
StatusBookmark : & statusBookmarkDB {
conn : conn ,
state : state ,
} ,
StatusFave : & statusFaveDB {
conn : conn ,
state : state ,
} ,
2022-12-08 17:35:14 +00:00
Timeline : & timelineDB {
conn : conn ,
state : state ,
} ,
User : & userDB {
conn : conn ,
state : state ,
} ,
Tombstone : & tombstoneDB {
conn : conn ,
state : state ,
} ,
conn : conn ,
2021-04-01 18:46:45 +00:00
}
2021-03-02 17:26:30 +00:00
2021-08-25 13:34:33 +00:00
// we can confidently return this useable service now
2021-04-01 18:46:45 +00:00
return ps , nil
2021-03-22 21:26:54 +00:00
}
2023-01-26 14:12:48 +00:00
func pgConn ( ctx context . Context ) ( * DBConn , error ) {
opts , err := deriveBunDBPGOptions ( ) //nolint:contextcheck
if err != nil {
return nil , fmt . Errorf ( "could not create bundb postgres options: %s" , err )
}
sqldb := stdlib . OpenDB ( * opts )
// Tune db connections for postgres, see:
// - https://bun.uptrace.dev/guide/running-bun-in-production.html#database-sql
// - https://www.alexedwards.net/blog/configuring-sqldb
sqldb . SetMaxOpenConns ( maxOpenConns ( ) ) // x number of conns per CPU
sqldb . SetMaxIdleConns ( 2 ) // assume default 2; if max idle is less than max open, it will be automatically adjusted
sqldb . SetConnMaxLifetime ( 5 * time . Minute ) // fine to kill old connections
conn := WrapDBConn ( bun . NewDB ( sqldb , pgdialect . New ( ) ) )
// ping to check the db is there and listening
if err := conn . PingContext ( ctx ) ; err != nil {
return nil , fmt . Errorf ( "postgres ping: %s" , err )
}
2023-02-17 11:02:29 +00:00
log . Info ( ctx , "connected to POSTGRES database" )
2023-01-26 14:12:48 +00:00
return conn , nil
}
2021-12-07 12:31:39 +00:00
func sqliteConn ( ctx context . Context ) ( * DBConn , error ) {
2022-01-30 16:06:28 +00:00
// validate db address has actually been set
2023-01-17 12:29:44 +00:00
address := config . GetDbAddress ( )
if address == "" {
2022-05-30 12:41:24 +00:00
return nil , fmt . Errorf ( "'%s' was not set when attempting to start sqlite" , config . DbAddressFlag ( ) )
2022-01-30 16:06:28 +00:00
}
2021-12-07 12:31:39 +00:00
2021-11-21 16:41:51 +00:00
// Drop anything fancy from DB address
2023-01-31 12:46:45 +00:00
address = strings . Split ( address , "?" ) [ 0 ] // drop any provided query strings
address = strings . TrimPrefix ( address , "file:" ) // we'll prepend this later ourselves
// build our own SQLite preferences
prefs := [ ] string {
// use immediate transaction lock mode to fail quickly if tx can't lock
// see https://pkg.go.dev/modernc.org/sqlite#Driver.Open
"_txlock=immediate" ,
}
2021-11-21 16:41:51 +00:00
2023-01-31 12:46:45 +00:00
if address == ":memory:" {
2023-02-17 11:02:29 +00:00
log . Warn ( ctx , "using sqlite in-memory mode; all data will be deleted when gts shuts down; this mode should only be used for debugging or running tests" )
2021-11-21 16:41:51 +00:00
2023-01-31 12:46:45 +00:00
// Use random name for in-memory instead of ':memory:', so
// multiple in-mem databases can be created without conflict.
address = uuid . NewString ( )
// in-mem-specific preferences
prefs = append ( prefs , [ ] string {
"mode=memory" , // indicate in-memory mode using query
"cache=shared" , // shared cache so that tests don't fail
} ... )
}
// rebuild address string with our derived preferences
address = "file:" + address
for i , q := range prefs {
var prefix string
if i == 0 {
prefix = "?"
} else {
prefix = "&"
}
address += prefix + q
2022-08-22 09:21:36 +00:00
}
2021-11-21 16:41:51 +00:00
// Open new DB instance
2023-01-17 12:29:44 +00:00
sqldb , err := sql . Open ( "sqlite" , address )
2021-11-21 16:41:51 +00:00
if err != nil {
if errWithCode , ok := err . ( * sqlite . Error ) ; ok {
err = errors . New ( sqlite . ErrorCodeString [ errWithCode . Code ( ) ] )
}
2023-01-31 12:46:45 +00:00
return nil , fmt . Errorf ( "could not open sqlite db with address %s: %w" , address , err )
2021-11-21 16:41:51 +00:00
}
2023-01-26 14:12:48 +00:00
// Tune db connections for sqlite, see:
// - https://bun.uptrace.dev/guide/running-bun-in-production.html#database-sql
// - https://www.alexedwards.net/blog/configuring-sqldb
2023-02-01 10:55:34 +00:00
sqldb . SetMaxOpenConns ( 1 ) // only 1 connection regardless of multiplier, see https://github.com/superseriousbusiness/gotosocial/issues/1407
sqldb . SetMaxIdleConns ( 1 ) // only keep max 1 idle connection around
sqldb . SetConnMaxLifetime ( 0 ) // don't kill connections due to age
2021-11-21 16:41:51 +00:00
2023-01-17 12:29:44 +00:00
// Wrap Bun database conn in our own wrapper
2021-11-21 16:41:51 +00:00
conn := WrapDBConn ( bun . NewDB ( sqldb , sqlitedialect . New ( ) ) )
// ping to check the db is there and listening
if err := conn . PingContext ( ctx ) ; err != nil {
if errWithCode , ok := err . ( * sqlite . Error ) ; ok {
err = errors . New ( sqlite . ErrorCodeString [ errWithCode . Code ( ) ] )
}
return nil , fmt . Errorf ( "sqlite ping: %s" , err )
}
2023-02-17 11:02:29 +00:00
log . Infof ( ctx , "connected to SQLITE database with address %s" , address )
2023-01-17 12:29:44 +00:00
2021-11-21 16:41:51 +00:00
return conn , nil
}
2021-03-02 17:26:30 +00:00
/ *
HANDY STUFF
* /
2023-01-26 14:12:48 +00:00
// maxOpenConns returns multiplier * GOMAXPROCS,
2023-01-31 12:46:45 +00:00
// returning just 1 instead if multiplier < 1.
2023-01-26 14:12:48 +00:00
func maxOpenConns ( ) int {
multiplier := config . GetDbMaxOpenConnsMultiplier ( )
if multiplier < 1 {
2023-01-31 12:46:45 +00:00
return 1
2023-01-26 14:12:48 +00:00
}
return multiplier * runtime . GOMAXPROCS ( 0 )
}
2021-08-25 13:34:33 +00:00
// deriveBunDBPGOptions takes an application config and returns either a ready-to-use set of options
2021-03-02 17:26:30 +00:00
// with sensible defaults, or an error if it's not satisfied by the provided config.
2021-12-07 12:31:39 +00:00
func deriveBunDBPGOptions ( ) ( * pgx . ConnConfig , error ) {
2021-12-21 11:08:27 +00:00
// these are all optional, the db adapter figures out defaults
2022-05-30 12:41:24 +00:00
address := config . GetDbAddress ( )
2021-03-02 21:52:31 +00:00
// validate database
2022-05-30 12:41:24 +00:00
database := config . GetDbDatabase ( )
2021-12-07 12:31:39 +00:00
if database == "" {
2021-03-04 11:07:24 +00:00
return nil , errors . New ( "no database set" )
2021-03-02 17:26:30 +00:00
}
2021-07-19 16:03:07 +00:00
var tlsConfig * tls . Config
2022-05-30 12:41:24 +00:00
switch config . GetDbTLSMode ( ) {
2023-01-17 12:29:44 +00:00
case "" , "disable" :
2021-07-19 16:03:07 +00:00
break // nothing to do
2023-01-17 12:29:44 +00:00
case "enable" :
2021-11-22 07:46:19 +00:00
/* #nosec G402 */
2021-07-19 16:03:07 +00:00
tlsConfig = & tls . Config {
InsecureSkipVerify : true ,
}
2023-01-17 12:29:44 +00:00
case "require" :
2021-07-19 16:03:07 +00:00
tlsConfig = & tls . Config {
InsecureSkipVerify : false ,
2022-05-30 12:41:24 +00:00
ServerName : address ,
2021-11-22 07:46:19 +00:00
MinVersion : tls . VersionTLS12 ,
2021-07-19 16:03:07 +00:00
}
}
2022-05-30 12:41:24 +00:00
if certPath := config . GetDbTLSCACert ( ) ; tlsConfig != nil && certPath != "" {
2021-07-19 16:03:07 +00:00
// load the system cert pool first -- we'll append the given CA cert to this
certPool , err := x509 . SystemCertPool ( )
if err != nil {
return nil , fmt . Errorf ( "error fetching system CA cert pool: %s" , err )
}
// open the file itself and make sure there's something in it
2022-05-30 12:41:24 +00:00
caCertBytes , err := os . ReadFile ( certPath )
2021-07-19 16:03:07 +00:00
if err != nil {
2022-05-30 12:41:24 +00:00
return nil , fmt . Errorf ( "error opening CA certificate at %s: %s" , certPath , err )
2021-07-19 16:03:07 +00:00
}
if len ( caCertBytes ) == 0 {
2022-05-30 12:41:24 +00:00
return nil , fmt . Errorf ( "ca cert at %s was empty" , certPath )
2021-07-19 16:03:07 +00:00
}
// make sure we have a PEM block
caPem , _ := pem . Decode ( caCertBytes )
if caPem == nil {
2022-05-30 12:41:24 +00:00
return nil , fmt . Errorf ( "could not parse cert at %s into PEM" , certPath )
2021-07-19 16:03:07 +00:00
}
// parse the PEM block into the certificate
caCert , err := x509 . ParseCertificate ( caPem . Bytes )
if err != nil {
2022-05-30 12:41:24 +00:00
return nil , fmt . Errorf ( "could not parse cert at %s into x509 certificate: %s" , certPath , err )
2021-07-19 16:03:07 +00:00
}
// we're happy, add it to the existing pool and then use this pool in our tls config
certPool . AddCert ( caCert )
tlsConfig . RootCAs = certPool
}
2021-08-25 13:34:33 +00:00
cfg , _ := pgx . ParseConfig ( "" )
2021-12-21 11:08:27 +00:00
if address != "" {
cfg . Host = address
}
2022-06-03 13:40:38 +00:00
if port := config . GetDbPort ( ) ; port > 0 {
2021-12-21 11:08:27 +00:00
cfg . Port = uint16 ( port )
}
2022-05-30 12:41:24 +00:00
if u := config . GetDbUser ( ) ; u != "" {
cfg . User = u
2021-12-21 11:08:27 +00:00
}
2022-05-30 12:41:24 +00:00
if p := config . GetDbPassword ( ) ; p != "" {
cfg . Password = p
2021-12-21 11:08:27 +00:00
}
if tlsConfig != nil {
cfg . TLSConfig = tlsConfig
}
2021-12-07 12:31:39 +00:00
cfg . Database = database
2021-08-25 13:34:33 +00:00
cfg . PreferSimpleProtocol = true
2022-05-30 12:41:24 +00:00
cfg . RuntimeParams [ "application_name" ] = config . GetApplicationName ( )
2021-03-02 17:26:30 +00:00
2021-08-25 13:34:33 +00:00
return cfg , nil
2021-03-02 17:26:30 +00:00
}
2023-01-26 14:12:48 +00:00
// sqlitePragmas sets desired sqlite pragmas based on configured values, and
// logs the results of the pragma queries. Errors if something goes wrong.
func sqlitePragmas ( ctx context . Context , conn * DBConn ) error {
var pragmas [ ] [ ] string
if mode := config . GetDbSqliteJournalMode ( ) ; mode != "" {
// Set the user provided SQLite journal mode
pragmas = append ( pragmas , [ ] string { "journal_mode" , mode } )
}
if mode := config . GetDbSqliteSynchronous ( ) ; mode != "" {
// Set the user provided SQLite synchronous mode
pragmas = append ( pragmas , [ ] string { "synchronous" , mode } )
}
if size := config . GetDbSqliteCacheSize ( ) ; size > 0 {
// Set the user provided SQLite cache size (in kibibytes)
// Prepend a '-' character to this to indicate to sqlite
// that we're giving kibibytes rather than num pages.
// https://www.sqlite.org/pragma.html#pragma_cache_size
s := "-" + strconv . FormatUint ( uint64 ( size / bytesize . KiB ) , 10 )
pragmas = append ( pragmas , [ ] string { "cache_size" , s } )
}
if timeout := config . GetDbSqliteBusyTimeout ( ) ; timeout > 0 {
t := strconv . FormatInt ( timeout . Milliseconds ( ) , 10 )
pragmas = append ( pragmas , [ ] string { "busy_timeout" , t } )
}
for _ , p := range pragmas {
pk := p [ 0 ]
pv := p [ 1 ]
if _ , err := conn . DB . ExecContext ( ctx , "PRAGMA ?=?" , bun . Ident ( pk ) , bun . Safe ( pv ) ) ; err != nil {
return fmt . Errorf ( "error executing sqlite pragma %s: %w" , pk , err )
}
var res string
if err := conn . DB . NewRaw ( "PRAGMA ?" , bun . Ident ( pk ) ) . Scan ( ctx , & res ) ; err != nil {
return fmt . Errorf ( "error scanning sqlite pragma %s: %w" , pv , err )
}
2023-02-17 11:02:29 +00:00
log . Infof ( ctx , "sqlite pragma %s set to %s" , pk , res )
2023-01-26 14:12:48 +00:00
}
return nil
}
2021-04-19 17:42:19 +00:00
/ *
CONVERSION FUNCTIONS
* /
2021-04-01 18:46:45 +00:00
2023-02-03 10:58:58 +00:00
func ( dbService * DBService ) TagStringToTag ( ctx context . Context , t string , originAccountID string ) ( * gtsmodel . Tag , error ) {
2022-05-30 12:41:24 +00:00
protocol := config . GetProtocol ( )
host := config . GetHost ( )
2023-02-03 10:58:58 +00:00
now := time . Now ( )
2021-12-07 12:31:39 +00:00
2023-02-03 10:58:58 +00:00
tag := & gtsmodel . Tag { }
// we can use selectorinsert here to create the new tag if it doesn't exist already
// inserted will be true if this is a new tag we just created
if err := dbService . conn . NewSelect ( ) . Model ( tag ) . Where ( "LOWER(?) = LOWER(?)" , bun . Ident ( "name" ) , t ) . Scan ( ctx ) ; err != nil && err != sql . ErrNoRows {
return nil , fmt . Errorf ( "error getting tag with name %s: %s" , t , err )
}
2021-04-19 17:42:19 +00:00
2023-02-03 10:58:58 +00:00
if tag . ID == "" {
// tag doesn't exist yet so populate it
newID , err := id . NewRandomULID ( )
if err != nil {
return nil , err
2021-04-19 17:42:19 +00:00
}
2023-02-03 10:58:58 +00:00
tag . ID = newID
tag . URL = protocol + "://" + host + "/tags/" + t
tag . Name = t
tag . FirstSeenFromAccountID = originAccountID
tag . CreatedAt = now
tag . UpdatedAt = now
useable := true
tag . Useable = & useable
listable := true
tag . Listable = & listable
}
// bail already if the tag isn't useable
if ! * tag . Useable {
return nil , fmt . Errorf ( "tag %s is not useable" , t )
2021-04-19 17:42:19 +00:00
}
2023-02-03 10:58:58 +00:00
tag . LastStatusAt = now
return tag , nil
2021-04-19 17:42:19 +00:00
}