mirror of
https://github.com/Eggbertx/gochan.git
synced 2025-08-26 10:36:23 -07:00
Merge branch 'master' into staff-rank-update
This commit is contained in:
commit
c5aa7a438d
172 changed files with 7301 additions and 5089 deletions
30
.github/workflows/go.yml
vendored
Normal file
30
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# This workflow will build a golang project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Build
|
||||
run: ./build.py
|
||||
|
||||
- name: Build plugins
|
||||
run: ./build.py build --plugin ./examples/plugins/geoip-legacy/ &&./build.py build --plugin ./examples/plugins/gochaninfo-mgmt-action/ && ./build.py build --plugin ./examples/plugins/ip2location/
|
||||
|
||||
- name: Test
|
||||
run: go test -cover -v ./pkg/... ./cmd/...
|
35
.gitignore
vendored
35
.gitignore
vendored
|
@ -1,23 +1,30 @@
|
|||
/gochan*
|
||||
/lib/
|
||||
/log/
|
||||
/releases/
|
||||
/vagrant/.vagrant/
|
||||
/html/boards.json
|
||||
*.bak
|
||||
/html/index.html
|
||||
/html/test*
|
||||
/html/js/
|
||||
/templates/override
|
||||
*.bak
|
||||
*.log
|
||||
/html/boards.json
|
||||
.vscode/settings.json
|
||||
.parcel-cache
|
||||
*.swp
|
||||
*.db
|
||||
.vagrant/
|
||||
*.log
|
||||
/templates/override
|
||||
|
||||
# Go output
|
||||
/gochan*
|
||||
/releases/
|
||||
*.so
|
||||
__debug_bin
|
||||
__debug_bin*
|
||||
|
||||
# Node.js/TypeScript
|
||||
/html/js/
|
||||
node_modules
|
||||
/frontend/coverage
|
||||
/frontend/tests/coverage
|
||||
.parcel-cache
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
.venv/
|
||||
.vscode/settings.json
|
||||
|
||||
# SQLite
|
||||
*.db
|
||||
*.*-journal
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
@ -11,12 +11,7 @@
|
|||
"mode": "auto",
|
||||
"cwd":"${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/cmd/gochan/",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"dlvLoadConfig": {
|
||||
"maxStringLen": 5000,
|
||||
"maxArrayValues": 500
|
||||
},
|
||||
"showGlobalVariables": true
|
||||
},
|
||||
{
|
||||
"name": "gochan-migration",
|
||||
|
@ -25,12 +20,7 @@
|
|||
"mode": "auto",
|
||||
"cwd":"${workspaceFolder}",
|
||||
"program": "${workspaceFolder}/cmd/gochan-migration/",
|
||||
"env": {},
|
||||
"args": [],
|
||||
"dlvLoadConfig": {
|
||||
"maxStringLen": 5000,
|
||||
"maxArrayValues": 500
|
||||
},
|
||||
"showGlobalVariables": true
|
||||
}
|
||||
]
|
||||
}
|
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright (c) 2013-2024, Gochan development group
|
||||
Copyright (c) 2013-2025, Gochan development group
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -60,11 +60,10 @@ See [`frontend/README.md`](frontend/README.md) for information on working with S
|
|||
## Roadmap
|
||||
|
||||
### Near future
|
||||
* Fully implement cyclical threads
|
||||
* Implement +50
|
||||
* Add more banners
|
||||
* Add more plugin support (more event triggers)
|
||||
|
||||
### Lower priority
|
||||
* RSS feeds from boards/specific threads/specific usernames+tripcodes (such as newsanon)
|
||||
* Pinning a post within a thread even if its not the OP, to prevent its deletion in a cyclical thread.
|
||||
* Pinning a post within a thread even if its not the OP, to prevent its deletion in a cyclic thread.
|
||||
|
|
18
build.py
18
build.py
|
@ -39,7 +39,7 @@ release_files = (
|
|||
"README.md",
|
||||
)
|
||||
|
||||
GOCHAN_VERSION = "4.0.1"
|
||||
GOCHAN_VERSION = "4.1.0"
|
||||
DATABASE_VERSION = "4" # stored in DBNAME.DBPREFIXdatabase_version
|
||||
|
||||
PATH_NOTHING = -1
|
||||
|
@ -425,15 +425,13 @@ def sass(watch=False):
|
|||
sys.exit(status)
|
||||
|
||||
def test(verbose=False, coverage=False):
|
||||
pkgs = os.listdir("pkg")
|
||||
for pkg in pkgs:
|
||||
cmd = ["go", "test"]
|
||||
if verbose:
|
||||
cmd += ["-v"]
|
||||
if coverage:
|
||||
cmd += ["-cover"]
|
||||
cmd += [path.join("./pkg", pkg)]
|
||||
run_cmd(cmd, realtime=True, print_command=True)
|
||||
cmd = ["go", "test"]
|
||||
if verbose:
|
||||
cmd += ["-v"]
|
||||
if coverage:
|
||||
cmd += ["-cover"]
|
||||
cmd += ["./pkg/...", "./cmd/..."]
|
||||
run_cmd(cmd, realtime=True, print_command=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -25,10 +25,14 @@ func (me *MigrationError) OldChanType() string {
|
|||
|
||||
func (me *MigrationError) Error() string {
|
||||
from := me.oldChanType
|
||||
errStr := "unable to migrate"
|
||||
if from != "" {
|
||||
from = " from " + from
|
||||
errStr += " from " + from
|
||||
}
|
||||
return "unable to migrate " + from + ": " + me.errMessage
|
||||
if me.errMessage != "" {
|
||||
errStr += ": " + me.errMessage
|
||||
}
|
||||
return errStr
|
||||
}
|
||||
|
||||
func NewMigrationError(oldChanType string, errMessage string) *MigrationError {
|
||||
|
@ -41,48 +45,45 @@ type MigrationOptions struct {
|
|||
OldChanConfig string
|
||||
OldDBName string
|
||||
NewDBName string
|
||||
DirAction int
|
||||
}
|
||||
|
||||
// DBMigrator is used for handling the migration from one database type to a
|
||||
// database compatible with gochan 3.x onward
|
||||
// database compatible with the latest gochan database version
|
||||
type DBMigrator interface {
|
||||
// Init sets the variables for connecting to the databases
|
||||
// Init sets up the migrator and sets up the database connection(s)
|
||||
Init(options *MigrationOptions) error
|
||||
|
||||
// IsMigrated checks to see if the database has already been migrated and quits if it has
|
||||
// and returns any errors that aren't "table doesn't exist". if the boolean value is true,
|
||||
// it can be assumed that the database has already been migrated and gochan-migration
|
||||
// will exit
|
||||
// IsMigrated returns true if the database is already migrated, and an error if any occured,
|
||||
// excluding missing table errors
|
||||
IsMigrated() (bool, error)
|
||||
|
||||
// MigrateDB alters the database schema to match the new schema, then migrates the imageboard
|
||||
// data (posts, boards, etc) to the new database. It is assumed that MigrateDB will handle
|
||||
// logging any errors that occur during the migration
|
||||
// IsMigratingInPlace returns true if the source database and the destination database are both the
|
||||
// same installation, meaning both have the same host/connection, database, table and prefix, meaning that
|
||||
// the tables will be altered during the migration to match the new schema, instead of creating tables in
|
||||
// the destination database and copying data over
|
||||
IsMigratingInPlace() bool
|
||||
|
||||
// MigrateDB handles migration of the source database, altering it in place or migrating it to the configured
|
||||
// gochan database. It returns true if the database is already migrated and an error if any occured. It is
|
||||
// assumed that MigrateDB implementations will handle logging any errors that occur during the migration
|
||||
MigrateDB() (bool, error)
|
||||
|
||||
// MigrateBoards gets info about the old boards in the board table and inserts each one
|
||||
// into the new database if they don't already exist
|
||||
// MigrateBoards migrates the board sections and boards if each one doesn't already exists
|
||||
MigrateBoards() error
|
||||
|
||||
// MigratePosts gets the threads and replies in the old database, and inserts them into
|
||||
// the new database, creating new threads to avoid putting replies in threads that already
|
||||
// exist
|
||||
// MigratePosts migrates the threads and replies (excluding deleted ones), creating new threads where necessary
|
||||
MigratePosts() error
|
||||
|
||||
// MigrateStaff gets the staff list in the old board and inserts them into the new board if
|
||||
// the username doesn't already exist. It sets the starting password to the given password
|
||||
MigrateStaff(password string) error
|
||||
// MigrateStaff migrates the staff, creating new staff accounts that don't already exist. Accounts created by this
|
||||
// will need to have their password reset in order to be logged into
|
||||
MigrateStaff() error
|
||||
|
||||
// MigrateBans gets the list of bans and appeals in the old database and inserts them into the
|
||||
// new one if, for each entry, the IP/name/etc isn't already banned for the same length
|
||||
// e.g. 1.1.1.1 is permabanned on both, 1.1.2.2 is banned for 5 days on both, etc
|
||||
// MigrateBans migrates IP bans, appeals, and filters
|
||||
MigrateBans() error
|
||||
|
||||
// MigrateAnnouncements gets the list of public and staff announcements in the old database
|
||||
// and inserts them into the new database,
|
||||
// MigrateAnnouncements migrates the list of public and staff announcements, if applicable
|
||||
MigrateAnnouncements() error
|
||||
|
||||
// Close closes the database if initialized and deltes the temporary columns created
|
||||
// Close closes the database if initialized and deletes any temporary columns created
|
||||
Close() error
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
|
@ -21,6 +22,16 @@ var (
|
|||
migrationLog zerolog.Logger
|
||||
)
|
||||
|
||||
func InitTestMigrationLog(t *testing.T) (err error) {
|
||||
dir := os.TempDir()
|
||||
migrationLogFile, err = os.CreateTemp(dir, "migration-test")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
migrationLog = zerolog.New(zerolog.NewTestWriter(t))
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitMigrationLog() (err error) {
|
||||
if migrationLogFile != nil {
|
||||
// Migration log already initialized
|
||||
|
|
|
@ -88,7 +88,7 @@ func RunSQLFile(path string, db *gcsql.GCDB) error {
|
|||
for _, statement := range sqlArr {
|
||||
statement = strings.TrimSpace(statement)
|
||||
if len(statement) > 0 {
|
||||
if _, err = db.ExecSQL(statement); err != nil {
|
||||
if _, err = db.Exec(nil, statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ type filenameOrUsernameBanBase struct {
|
|||
StaffID int // sql: staff_id
|
||||
StaffNote string // sql: staff_note
|
||||
IssuedAt time.Time // sql: issued_at
|
||||
check string // replaced with username or filename
|
||||
IsRegex bool // sql: is_regex
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package gcupdate
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -22,6 +21,11 @@ type GCDatabaseUpdater struct {
|
|||
TargetDBVer int
|
||||
}
|
||||
|
||||
// IsMigratingInPlace implements common.DBMigrator.
|
||||
func (*GCDatabaseUpdater) IsMigratingInPlace() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (dbu *GCDatabaseUpdater) Init(options *common.MigrationOptions) error {
|
||||
dbu.options = options
|
||||
sqlCfg := config.GetSQLConfig()
|
||||
|
@ -32,7 +36,7 @@ func (dbu *GCDatabaseUpdater) Init(options *common.MigrationOptions) error {
|
|||
|
||||
func (dbu *GCDatabaseUpdater) IsMigrated() (bool, error) {
|
||||
var currentDatabaseVersion int
|
||||
err := dbu.db.QueryRowSQL(`SELECT version FROM DBPREFIXdatabase_version WHERE component = 'gochan'`, nil,
|
||||
err := dbu.db.QueryRow(nil, "SELECT version FROM DBPREFIXdatabase_version WHERE component = 'gochan'", nil,
|
||||
[]any{¤tDatabaseVersion})
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -174,7 +178,7 @@ func (dbu *GCDatabaseUpdater) migrateFileBans(ctx context.Context, sqlConfig *co
|
|||
tx, err := dbu.db.BeginTx(ctx, nil)
|
||||
defer func() {
|
||||
if a := recover(); a != nil {
|
||||
err = errors.New(fmt.Sprintf("recovered: %v", a))
|
||||
err = fmt.Errorf("recovered: %v", a)
|
||||
errEv.Caller(4).Err(err).Send()
|
||||
errEv.Discard()
|
||||
} else if err != nil {
|
||||
|
@ -262,7 +266,7 @@ func (dbu *GCDatabaseUpdater) migrateFilenameBans(ctx context.Context, _ *config
|
|||
tx, err := dbu.db.BeginTx(ctx, nil)
|
||||
defer func() {
|
||||
if a := recover(); a != nil {
|
||||
err = errors.New(fmt.Sprintf("recovered: %v", a))
|
||||
err = fmt.Errorf("recovered: %v", a)
|
||||
errEv.Caller(4).Err(err).Send()
|
||||
errEv.Discard()
|
||||
} else if err != nil {
|
||||
|
@ -320,7 +324,7 @@ func (dbu *GCDatabaseUpdater) migrateUsernameBans(ctx context.Context, _ *config
|
|||
tx, err := dbu.db.BeginTx(ctx, nil)
|
||||
defer func() {
|
||||
if a := recover(); a != nil {
|
||||
err = errors.New(fmt.Sprintf("recovered: %v", a))
|
||||
err = fmt.Errorf("recovered: %v", a)
|
||||
errEv.Caller(4).Err(err).Send()
|
||||
errEv.Discard()
|
||||
} else if err != nil {
|
||||
|
@ -378,7 +382,7 @@ func (dbu *GCDatabaseUpdater) migrateWordfilters(ctx context.Context, sqlConfig
|
|||
tx, err := dbu.db.BeginTx(ctx, nil)
|
||||
defer func() {
|
||||
if a := recover(); a != nil {
|
||||
err = errors.New(fmt.Sprintf("recovered: %v", a))
|
||||
err = fmt.Errorf("recovered: %v", a)
|
||||
errEv.Caller(4).Err(err).Send()
|
||||
errEv.Discard()
|
||||
} else if err != nil {
|
||||
|
@ -471,7 +475,7 @@ func (*GCDatabaseUpdater) MigratePosts() error {
|
|||
return gcutil.ErrNotImplemented
|
||||
}
|
||||
|
||||
func (*GCDatabaseUpdater) MigrateStaff(_ string) error {
|
||||
func (*GCDatabaseUpdater) MigrateStaff() error {
|
||||
return gcutil.ErrNotImplemented
|
||||
}
|
||||
|
||||
|
|
66
cmd/gochan-migration/internal/pre2021/announcements.go
Normal file
66
cmd/gochan-migration/internal/pre2021/announcements.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
)
|
||||
|
||||
type migrationAnnouncement struct {
|
||||
gcsql.Announcement
|
||||
oldPoster string
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) MigrateAnnouncements() error {
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
|
||||
rows, err := m.db.Query(nil, announcementsQuery)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to get announcements")
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if _, err = m.getMigrationUser(errEv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var oldAnnouncements []migrationAnnouncement
|
||||
|
||||
for rows.Next() {
|
||||
var announcement migrationAnnouncement
|
||||
|
||||
if err = rows.Scan(&announcement.ID, &announcement.Subject, &announcement.Message, &announcement.oldPoster, &announcement.Timestamp); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to scan announcement row")
|
||||
return err
|
||||
}
|
||||
oldAnnouncements = append(oldAnnouncements, announcement)
|
||||
}
|
||||
for _, announcement := range oldAnnouncements {
|
||||
announcement.StaffID, err = gcsql.GetStaffID(announcement.oldPoster)
|
||||
if errors.Is(err, gcsql.ErrUnrecognizedUsername) {
|
||||
// user doesn't exist, use migration user
|
||||
common.LogWarning().Str("staff", announcement.oldPoster).Msg("Staff username not found in database")
|
||||
announcement.Message += "\n(originally by " + announcement.oldPoster + ")"
|
||||
announcement.StaffID = m.migrationUser.ID
|
||||
} else if err != nil {
|
||||
errEv.Err(err).Caller().Str("staff", announcement.oldPoster).Msg("Failed to get staff ID")
|
||||
return err
|
||||
}
|
||||
if _, err = gcsql.Exec(nil,
|
||||
"INSERT INTO DBPREFIXannouncements(staff_id,subject,message,timestamp) values(?,?,?,?)",
|
||||
announcement.StaffID, announcement.Subject, announcement.Message, announcement.Timestamp,
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().Str("staff", announcement.oldPoster).Msg("Failed to migrate announcement")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = rows.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to close announcement rows")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
32
cmd/gochan-migration/internal/pre2021/announcements_test.go
Normal file
32
cmd/gochan-migration/internal/pre2021/announcements_test.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigrateAnnouncements(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, false)
|
||||
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateBoards()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateStaff()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateAnnouncements()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
var numAnnouncements int
|
||||
assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXannouncements WHERE staff_id > 0", nil, []any{&numAnnouncements}))
|
||||
assert.Equal(t, 2, numAnnouncements, "Expected to have two announcement")
|
||||
}
|
232
cmd/gochan-migration/internal/pre2021/bans.go
Normal file
232
cmd/gochan-migration/internal/pre2021/bans.go
Normal file
|
@ -0,0 +1,232 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type migrationBan struct {
|
||||
oldID int
|
||||
allowRead string
|
||||
ip string
|
||||
name string
|
||||
nameIsRegex bool
|
||||
filename string
|
||||
fileChecksum string
|
||||
boards string
|
||||
staff string
|
||||
timestamp time.Time
|
||||
expires time.Time
|
||||
permaban bool
|
||||
reason string
|
||||
banType int
|
||||
staffNote string
|
||||
appealAt time.Time
|
||||
canAppeal bool
|
||||
|
||||
boardIDs []int
|
||||
staffID int
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) migrateBansInPlace() error {
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
initSQLPath := gcutil.FindResource("sql/initdb_" + m.db.SQLDriver() + ".sql")
|
||||
ba, err := os.ReadFile(initSQLPath)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("initDBFile", initSQLPath).
|
||||
Msg("Failed to read initdb file")
|
||||
return err
|
||||
}
|
||||
statements := strings.Split(string(ba), ";")
|
||||
for _, stmt := range statements {
|
||||
stmt = strings.TrimSpace(stmt)
|
||||
if strings.HasPrefix(stmt, "CREATE TABLE DBPREFIXip_ban") || strings.HasPrefix(stmt, "CREATE TABLE DBPREFIXfilter") {
|
||||
_, err = gcsql.Exec(nil, stmt)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("statement", stmt).
|
||||
Msg("Failed to create table")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
// since the table names are different, migrateBansToNewDB can be called directly to migrate bans
|
||||
return m.migrateBansToNewDB()
|
||||
}
|
||||
|
||||
func (*Pre2021Migrator) migrateBan(tx *sql.Tx, ban *migrationBan, boardID *int, errEv *zerolog.Event) error {
|
||||
migratedBan := &gcsql.IPBan{
|
||||
BoardID: boardID,
|
||||
RangeStart: ban.ip,
|
||||
RangeEnd: ban.ip,
|
||||
IssuedAt: ban.timestamp,
|
||||
}
|
||||
migratedBan.CanAppeal = ban.canAppeal
|
||||
migratedBan.AppealAt = ban.appealAt
|
||||
migratedBan.ExpiresAt = ban.expires
|
||||
migratedBan.Permanent = ban.permaban
|
||||
migratedBan.Message = ban.reason
|
||||
migratedBan.StaffID = ban.staffID
|
||||
migratedBan.StaffNote = ban.staffNote
|
||||
if err := gcsql.NewIPBan(migratedBan, &gcsql.RequestOptions{Tx: tx}); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("oldID", ban.oldID).Msg("Failed to migrate ban")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) migrateBansToNewDB() error {
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
|
||||
tx, err := gcsql.BeginTx()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to start transaction")
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
rows, err := m.db.Query(nil, bansQuery)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to get bans")
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var ban migrationBan
|
||||
if err = rows.Scan(
|
||||
&ban.oldID, &ban.allowRead, &ban.ip, &ban.name, &ban.nameIsRegex, &ban.filename, &ban.fileChecksum,
|
||||
&ban.boards, &ban.staff, &ban.timestamp, &ban.expires, &ban.permaban, &ban.reason, &ban.banType, &ban.staffNote, &ban.appealAt, &ban.canAppeal,
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to scan ban row")
|
||||
return err
|
||||
}
|
||||
|
||||
if ban.boards != "" && ban.boards != "*" {
|
||||
boardDirs := strings.Split(ban.boards, ",")
|
||||
for _, dir := range boardDirs {
|
||||
dir = strings.TrimSpace(dir)
|
||||
boardID, err := gcsql.GetBoardIDFromDir(dir)
|
||||
if err != nil {
|
||||
if errors.Is(err, gcsql.ErrBoardDoesNotExist) {
|
||||
common.Logger().Warn().Str("board", dir).Msg("Found unrecognized ban board")
|
||||
continue
|
||||
} else {
|
||||
errEv.Err(err).Caller().Str("board", dir).Msg("Failed getting board ID from dir")
|
||||
return err
|
||||
}
|
||||
}
|
||||
ban.boardIDs = append(ban.boardIDs, boardID)
|
||||
}
|
||||
}
|
||||
|
||||
migrationUser, err := m.getMigrationUser(errEv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ban.staffID, err = gcsql.GetStaffID(ban.staff)
|
||||
if errors.Is(err, gcsql.ErrUnrecognizedUsername) {
|
||||
// username not found after staff were migrated, use a stand-in account to be updated by the admin later
|
||||
common.LogWarning().
|
||||
Str("username", ban.staff).
|
||||
Str("migrationUser", migrationUser.Username).
|
||||
Msg("Ban staff not found in migrated staff table, using migration user instead")
|
||||
ban.staffID = migrationUser.ID
|
||||
} else if err != nil {
|
||||
errEv.Err(err).Caller().Str("username", ban.staff).Msg("Failed to get staff from username")
|
||||
return err
|
||||
}
|
||||
|
||||
if ban.ip == "" && ban.name == "" && ban.fileChecksum == "" && ban.filename == "" {
|
||||
common.LogWarning().Int("banID", ban.oldID).Msg("Found invalid ban (no IP, name, file checksum, or filename set)")
|
||||
continue
|
||||
}
|
||||
if ban.ip != "" {
|
||||
if net.ParseIP(ban.ip) == nil {
|
||||
gcutil.LogWarning().
|
||||
Int("oldID", ban.oldID).
|
||||
Str("ip", ban.ip).
|
||||
Msg("Found ban with invalid IP address, skipping")
|
||||
continue
|
||||
}
|
||||
if len(ban.boardIDs) == 0 {
|
||||
if err = m.migrateBan(tx, &ban, nil, errEv); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
for b := range ban.boardIDs {
|
||||
if err = m.migrateBan(tx, &ban, &ban.boardIDs[b], errEv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ban.name != "" || ban.fileChecksum != "" || ban.filename != "" {
|
||||
filter := &gcsql.Filter{
|
||||
StaffID: &ban.staffID,
|
||||
StaffNote: ban.staffNote,
|
||||
IsActive: true,
|
||||
HandleIfAny: true,
|
||||
MatchAction: "reject",
|
||||
MatchDetail: ban.reason,
|
||||
}
|
||||
var conditions []gcsql.FilterCondition
|
||||
if ban.name != "" {
|
||||
nameCondition := gcsql.FilterCondition{
|
||||
Field: "name",
|
||||
Search: ban.name,
|
||||
MatchMode: gcsql.ExactMatch,
|
||||
}
|
||||
if ban.nameIsRegex {
|
||||
nameCondition.MatchMode = gcsql.RegexMatch
|
||||
}
|
||||
conditions = append(conditions, nameCondition)
|
||||
}
|
||||
if ban.fileChecksum != "" {
|
||||
conditions = append(conditions, gcsql.FilterCondition{
|
||||
Field: "checksum",
|
||||
MatchMode: gcsql.ExactMatch,
|
||||
Search: ban.fileChecksum,
|
||||
})
|
||||
}
|
||||
if ban.filename != "" {
|
||||
filenameCondition := gcsql.FilterCondition{
|
||||
Field: "filename",
|
||||
Search: ban.filename,
|
||||
MatchMode: gcsql.ExactMatch,
|
||||
}
|
||||
if ban.nameIsRegex {
|
||||
filenameCondition.MatchMode = gcsql.RegexMatch
|
||||
}
|
||||
conditions = append(conditions, filenameCondition)
|
||||
}
|
||||
if err = gcsql.ApplyFilterTx(context.Background(), tx, filter, conditions, ban.boardIDs); err != nil {
|
||||
errEv.Err(err).Caller().Int("banID", ban.oldID).Msg("Failed to migrate ban to filter")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) MigrateBans() error {
|
||||
if m.IsMigratingInPlace() {
|
||||
return m.migrateBansInPlace()
|
||||
}
|
||||
return m.migrateBansToNewDB()
|
||||
}
|
58
cmd/gochan-migration/internal/pre2021/bans_test.go
Normal file
58
cmd/gochan-migration/internal/pre2021/bans_test.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigrateBans(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, false)
|
||||
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateBoards()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigratePosts()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateStaff()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateBans()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
validateBanMigration(t)
|
||||
}
|
||||
|
||||
func validateBanMigration(t *testing.T) {
|
||||
bans, err := gcsql.GetIPBans(0, 200, false)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 6, len(bans), "Expected to have 4 valid bans")
|
||||
assert.NotZero(t, bans[0].StaffID, "Expected ban staff ID field to be set")
|
||||
|
||||
var numInvalidBans int
|
||||
assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXip_ban WHERE message = ?", []any{"Full ban on 8.8.0.0/16"}, []any{&numInvalidBans}))
|
||||
assert.Equal(t, 0, numInvalidBans, "Expected the invalid test to not be migrated")
|
||||
|
||||
filters, err := gcsql.GetAllFilters(gcsql.TrueOrFalse)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 1, len(filters))
|
||||
conditions, err := filters[0].Conditions()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 3, len(conditions), "Expected filter to have three conditions")
|
||||
}
|
|
@ -1,133 +1,205 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
)
|
||||
|
||||
func (m *Pre2021Migrator) MigrateBoards() error {
|
||||
if m.oldBoards == nil {
|
||||
m.oldBoards = map[int]string{}
|
||||
}
|
||||
if m.newBoards == nil {
|
||||
m.newBoards = map[int]string{}
|
||||
}
|
||||
// get all boards from new db
|
||||
err := gcsql.ResetBoardSectionArrays()
|
||||
type migrationBoard struct {
|
||||
oldSectionID int
|
||||
oldID int
|
||||
gcsql.Board
|
||||
}
|
||||
|
||||
type migrationSection struct {
|
||||
oldID int
|
||||
gcsql.Section
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) migrateSections() error {
|
||||
// creates sections in the new db if they don't exist, and also creates a migration section that
|
||||
// boards will be set to, to be moved to the correct section by the admin after migration
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
// populate m.sections with all sections from the new db
|
||||
currentAllSections, err := gcsql.GetAllSections(false)
|
||||
if err != nil {
|
||||
return nil
|
||||
errEv.Err(err).Caller().Msg("Failed to get all sections from new db")
|
||||
return err
|
||||
}
|
||||
|
||||
// get boards from old db
|
||||
rows, err := m.db.QuerySQL(boardsQuery)
|
||||
for _, section := range currentAllSections {
|
||||
m.sections = append(m.sections, migrationSection{
|
||||
oldID: -1,
|
||||
Section: section,
|
||||
})
|
||||
}
|
||||
|
||||
var sectionsToBeCreated []gcsql.Section
|
||||
rows, err := m.db.Query(nil, sectionsQuery)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to query old database sections")
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var dir string
|
||||
var title string
|
||||
var subtitle string
|
||||
var description string
|
||||
var section int
|
||||
var max_file_size int
|
||||
var max_pages int
|
||||
var default_style string
|
||||
var locked bool
|
||||
var anonymous string
|
||||
var forced_anon bool
|
||||
var max_age int
|
||||
var autosage_after int
|
||||
var no_images_after int
|
||||
var max_message_length int
|
||||
var embeds_allowed bool
|
||||
var redirect_to_thread bool
|
||||
var require_file bool
|
||||
var enable_catalog bool
|
||||
if err = rows.Scan(
|
||||
&id,
|
||||
&dir,
|
||||
&title,
|
||||
&subtitle,
|
||||
&description,
|
||||
§ion,
|
||||
&max_file_size,
|
||||
&max_pages,
|
||||
&default_style,
|
||||
&locked,
|
||||
&anonymous,
|
||||
&forced_anon,
|
||||
&max_age,
|
||||
&autosage_after,
|
||||
&no_images_after,
|
||||
&max_message_length,
|
||||
&embeds_allowed,
|
||||
&redirect_to_thread,
|
||||
&require_file,
|
||||
&enable_catalog); err != nil {
|
||||
var section gcsql.Section
|
||||
if err = rows.Scan(§ion.ID, §ion.Position, §ion.Hidden, §ion.Name, §ion.Abbreviation); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to scan row into section")
|
||||
return err
|
||||
}
|
||||
found := false
|
||||
for b := range gcsql.AllBoards {
|
||||
if _, ok := m.oldBoards[id]; !ok {
|
||||
m.oldBoards[id] = dir
|
||||
}
|
||||
if gcsql.AllBoards[b].Dir == dir {
|
||||
log.Printf("Board /%s/ already exists in new db, moving on\n", dir)
|
||||
var found bool
|
||||
for s, newSection := range m.sections {
|
||||
if section.Name == newSection.Name {
|
||||
// section already exists, update values
|
||||
m.sections[s].oldID = section.ID
|
||||
m.sections[s].Abbreviation = section.Abbreviation
|
||||
m.sections[s].Hidden = section.Hidden
|
||||
m.sections[s].Position = section.Position
|
||||
common.LogInfo().
|
||||
Int("sectionID", section.ID).
|
||||
Int("oldSectionID", m.sections[s].oldID).
|
||||
Str("sectionName", section.Name).
|
||||
Str("sectionAbbreviation", section.Abbreviation).
|
||||
Msg("Section already exists in new db, values will be updated")
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
sectionsToBeCreated = append(sectionsToBeCreated, section)
|
||||
}
|
||||
}
|
||||
if err = rows.Close(); err != nil {
|
||||
errEv.Caller().Msg("Failed to close section rows")
|
||||
return err
|
||||
}
|
||||
for _, section := range sectionsToBeCreated {
|
||||
migratedSection, err := gcsql.NewSection(section.Name, section.Abbreviation, section.Hidden, section.Position)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Str("sectionName", section.Name).Msg("Failed to migrate section")
|
||||
return err
|
||||
}
|
||||
m.sections = append(m.sections, migrationSection{
|
||||
Section: *migratedSection,
|
||||
})
|
||||
}
|
||||
|
||||
for s, section := range m.sections {
|
||||
if err = m.sections[s].UpdateValues(); err != nil {
|
||||
errEv.Err(err).Caller().Str("sectionName", section.Name).Msg("Failed to update pre-existing section values")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) MigrateBoards() error {
|
||||
m.boards = nil
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
|
||||
// get all boards from new db
|
||||
err := gcsql.ResetBoardSectionArrays()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to reset board section arrays")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = m.migrateSections(); err != nil {
|
||||
// error should already be logged by migrateSectionsToNewDB
|
||||
return err
|
||||
}
|
||||
|
||||
allBoards, err := gcsql.GetAllBoards(false)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to get all boards from new db")
|
||||
return err
|
||||
}
|
||||
for _, board := range allBoards {
|
||||
m.boards = append(m.boards, migrationBoard{
|
||||
oldSectionID: -1,
|
||||
oldID: -1,
|
||||
Board: board,
|
||||
})
|
||||
}
|
||||
|
||||
// get boards from old db
|
||||
rows, err := m.db.Query(nil, boardsQuery)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to query old database boards")
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
var boardsTmp []migrationBoard
|
||||
|
||||
for rows.Next() {
|
||||
var board migrationBoard
|
||||
var maxPages int
|
||||
if err = rows.Scan(
|
||||
&board.oldID, &board.NavbarPosition, &board.Dir, &board.Title, &board.Subtitle, &board.Description,
|
||||
&board.SectionID, &board.MaxFilesize, &maxPages, &board.DefaultStyle, &board.Locked, &board.CreatedAt,
|
||||
&board.AnonymousName, &board.ForceAnonymous, &board.AutosageAfter, &board.NoImagesAfter, &board.MaxMessageLength,
|
||||
&board.AllowEmbeds, &board.RedirectToThread, &board.RequireFile, &board.EnableCatalog,
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to scan row into board")
|
||||
return err
|
||||
}
|
||||
board.MaxThreads = maxPages * config.GetBoardConfig(board.Dir).ThreadsPerPage
|
||||
boardsTmp = append(boardsTmp, board)
|
||||
}
|
||||
|
||||
for _, board := range boardsTmp {
|
||||
found := false
|
||||
for b, newBoard := range m.boards {
|
||||
if newBoard.Dir == board.Dir {
|
||||
m.boards[b].oldID = board.oldID
|
||||
m.boards[b].oldSectionID = board.SectionID
|
||||
common.LogInfo().
|
||||
Str("board", board.Dir).
|
||||
Int("oldBoardID", board.ID).
|
||||
Int("migratedBoardID", newBoard.ID).
|
||||
Msg("Board already exists in new db, updating values")
|
||||
// don't update other values in the array since they don't affect migrating threads or posts
|
||||
if _, err = gcsql.Exec(nil, `UPDATE DBPREFIXboards
|
||||
SET uri = ?, navbar_position = ?, title = ?, subtitle = ?, description = ?,
|
||||
max_file_size = ?, max_threads = ?, default_style = ?, locked = ?,
|
||||
anonymous_name = ?, force_anonymous = ?, autosage_after = ?, no_images_after = ?, max_message_length = ?,
|
||||
min_message_length = ?, allow_embeds = ?, redirect_to_thread = ?, require_file = ?, enable_catalog = ?
|
||||
WHERE id = ?`,
|
||||
board.Dir, board.NavbarPosition, board.Title, board.Subtitle, board.Description,
|
||||
board.MaxFilesize, board.MaxThreads, board.DefaultStyle, board.Locked,
|
||||
board.AnonymousName, board.ForceAnonymous, board.AutosageAfter, board.NoImagesAfter, board.MaxMessageLength,
|
||||
board.MinMessageLength, board.AllowEmbeds, board.RedirectToThread, board.RequireFile, board.EnableCatalog,
|
||||
newBoard.ID); err != nil {
|
||||
errEv.Err(err).Caller().Str("board", board.Dir).Msg("Failed to update board values")
|
||||
return err
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
// create new board using the board data from the old db
|
||||
// omitting things like ID and creation date since we don't really care
|
||||
if err = gcsql.CreateBoard(&gcsql.Board{
|
||||
Dir: dir,
|
||||
Title: title,
|
||||
Subtitle: subtitle,
|
||||
Description: description,
|
||||
SectionID: section,
|
||||
MaxFilesize: max_file_size,
|
||||
// MaxPages: max_pages,
|
||||
DefaultStyle: default_style,
|
||||
Locked: locked,
|
||||
AnonymousName: anonymous,
|
||||
ForceAnonymous: forced_anon,
|
||||
// MaxAge: max_age,
|
||||
AutosageAfter: autosage_after,
|
||||
NoImagesAfter: no_images_after,
|
||||
MaxMessageLength: max_message_length,
|
||||
AllowEmbeds: embeds_allowed,
|
||||
RedirectToThread: redirect_to_thread,
|
||||
RequireFile: require_file,
|
||||
EnableCatalog: enable_catalog,
|
||||
}, false); err != nil {
|
||||
if err = gcsql.CreateBoard(&board.Board, board.IsHidden(false)); err != nil {
|
||||
errEv.Err(err).Caller().Str("board", board.Dir).Msg("Failed to create board")
|
||||
return err
|
||||
}
|
||||
m.newBoards[id] = dir
|
||||
log.Printf("/%s/ successfully migrated in the database", dir)
|
||||
// Automatic directory migration has the potential to go horribly wrong, so I'm leaving this
|
||||
// commented out for now
|
||||
// switch m.options.DirAction {
|
||||
// case common.DirCopy:
|
||||
|
||||
// case common.DirMove:
|
||||
// // move the old directory (probably should copy instead) to the new one
|
||||
// newDocumentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
// log.Println("Old board path:", path.Join(m.config.DocumentRoot, dir))
|
||||
// log.Println("Old board path:", path.Join(newDocumentRoot, dir))
|
||||
// if err = os.Rename(
|
||||
// path.Join(m.config.DocumentRoot, dir),
|
||||
// path.Join(newDocumentRoot, dir),
|
||||
// ); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// log.Printf("/%s/ directory/files successfully moved")
|
||||
// }
|
||||
m.boards = append(m.boards, board)
|
||||
common.LogInfo().
|
||||
Str("dir", board.Dir).
|
||||
Int("boardID", board.ID).
|
||||
Msg("Board successfully created")
|
||||
}
|
||||
if err = gcsql.ResetBoardSectionArrays(); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to reset board and section arrays")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
70
cmd/gochan-migration/internal/pre2021/boards_test.go
Normal file
70
cmd/gochan-migration/internal/pre2021/boards_test.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigrateBoards(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, false)
|
||||
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.NoError(t, gcsql.ResetBoardSectionArrays())
|
||||
|
||||
numBoards := len(gcsql.AllBoards)
|
||||
numSections := len(gcsql.AllSections)
|
||||
|
||||
assert.Equal(t, 1, numBoards, "Expected to have 1 board pre-migration (/test/ is automatically created during provisioning)")
|
||||
assert.Equal(t, 1, numSections, "Expected to have 1 section pre-migration (Main is automatically created during provisioning)")
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateBoards()) {
|
||||
t.FailNow()
|
||||
}
|
||||
validateBoardMigration(t)
|
||||
}
|
||||
|
||||
func validateBoardMigration(t *testing.T) {
|
||||
migratedBoards, err := gcsql.GetAllBoards(false)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
migratedSections, err := gcsql.GetAllSections(false)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
assert.Equal(t, len(migratedBoards), 3, "Expected updated boards list to have three boards")
|
||||
assert.Equal(t, len(migratedSections), 2, "Expected updated sections list to have two sections")
|
||||
|
||||
// Test migrated sections
|
||||
mainSection, err := gcsql.GetSectionFromName("Main")
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, "mainmigration", mainSection.Abbreviation, "Expected Main section to have updated abbreviation name 'mainmigration'")
|
||||
|
||||
// Test migrated boards
|
||||
testBoard, err := gcsql.GetBoardFromDir("test")
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Greater(t, testBoard.ID, 0)
|
||||
assert.Equal(t, "Testing Board", testBoard.Title)
|
||||
assert.Equal(t, "Board for testing pre-2021 migration", testBoard.Subtitle)
|
||||
assert.Equal(t, "Board for testing pre-2021 migration description", testBoard.Description)
|
||||
testBoardSection, err := gcsql.GetSectionFromID(testBoard.SectionID)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, "Main", testBoardSection.Name, "Expected /test/ board to be in Main section")
|
||||
|
||||
hiddenBoard, err := gcsql.GetBoardFromDir("hidden")
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, "Hidden Board", hiddenBoard.Title)
|
||||
}
|
|
@ -2,171 +2,186 @@ package pre2021
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type postTable struct {
|
||||
id int
|
||||
boardid int
|
||||
parentid int
|
||||
name string
|
||||
tripcode string
|
||||
email string
|
||||
subject string
|
||||
message string
|
||||
message_raw string
|
||||
password string
|
||||
filename string
|
||||
filename_original string
|
||||
file_checksum string
|
||||
filesize int
|
||||
image_w int
|
||||
image_h int
|
||||
thumb_w int
|
||||
thumb_h int
|
||||
ip string
|
||||
tag string
|
||||
timestamp time.Time
|
||||
autosage bool
|
||||
deleted_timestamp time.Time
|
||||
bumped time.Time
|
||||
stickied bool
|
||||
locked bool
|
||||
reviewed bool
|
||||
type migrationPost struct {
|
||||
gcsql.Post
|
||||
autosage bool
|
||||
bumped time.Time
|
||||
stickied bool
|
||||
locked bool
|
||||
|
||||
newBoardID int
|
||||
// oldParentID int
|
||||
filename string
|
||||
filenameOriginal string
|
||||
fileChecksum string
|
||||
filesize int
|
||||
imageW int
|
||||
imageH int
|
||||
thumbW int
|
||||
thumbH int
|
||||
|
||||
oldID int
|
||||
boardID int
|
||||
oldBoardID int
|
||||
oldParentID int
|
||||
}
|
||||
|
||||
func (*Pre2021Migrator) migratePost(tx *sql.Tx, post *migrationPost, errEv *zerolog.Event) error {
|
||||
var err error
|
||||
opts := &gcsql.RequestOptions{Tx: tx}
|
||||
if post.oldParentID == 0 {
|
||||
// migrating post was a thread OP, create the row in the threads table
|
||||
if post.ThreadID, err = gcsql.CreateThread(opts, post.boardID, false, post.stickied, post.autosage, false); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("boardID", post.boardID).
|
||||
Msg("Failed to create thread")
|
||||
}
|
||||
}
|
||||
|
||||
// insert thread top post
|
||||
if err = post.Insert(true, post.boardID, false, post.stickied, post.autosage, false, opts); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("boardID", post.boardID).
|
||||
Int("threadID", post.ThreadID).
|
||||
Msg("Failed to insert thread OP")
|
||||
}
|
||||
|
||||
if post.filename != "" {
|
||||
if err = post.AttachFile(&gcsql.Upload{
|
||||
PostID: post.ID,
|
||||
OriginalFilename: post.filenameOriginal,
|
||||
Filename: post.filename,
|
||||
Checksum: post.fileChecksum,
|
||||
FileSize: post.filesize,
|
||||
ThumbnailWidth: post.thumbW,
|
||||
ThumbnailHeight: post.thumbH,
|
||||
Width: post.imageW,
|
||||
Height: post.imageH,
|
||||
}, opts); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("oldPostID", post.oldID).
|
||||
Msg("Failed to attach upload to migrated post")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) MigratePosts() error {
|
||||
var err error
|
||||
if err = m.migrateThreads(); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.migratePostsUtil()
|
||||
}
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
|
||||
func (m *Pre2021Migrator) migrateThreads() error {
|
||||
tx, err := m.db.Begin()
|
||||
tx, err := gcsql.BeginTx()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to start transaction")
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := m.db.PrepareSQL(postsQuery, tx)
|
||||
rows, err := m.db.Query(nil, threadsQuery)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
errEv.Err(err).Caller().Msg("Failed to get threads")
|
||||
return err
|
||||
}
|
||||
rows, err := stmt.Query()
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
defer rows.Close()
|
||||
|
||||
var threadIDsWithInvalidBoards []int
|
||||
var missingBoardIDs []int
|
||||
var migratedThreads int
|
||||
for rows.Next() {
|
||||
var post postTable
|
||||
var thread migrationPost
|
||||
if err = rows.Scan(
|
||||
&post.id,
|
||||
&post.boardid,
|
||||
&post.parentid,
|
||||
&post.name,
|
||||
&post.tripcode,
|
||||
&post.email,
|
||||
&post.subject,
|
||||
&post.message,
|
||||
&post.message_raw,
|
||||
&post.password,
|
||||
&post.filename,
|
||||
&post.filename_original,
|
||||
&post.file_checksum,
|
||||
&post.filesize,
|
||||
&post.image_w,
|
||||
&post.image_h,
|
||||
&post.thumb_w,
|
||||
&post.thumb_h,
|
||||
&post.ip,
|
||||
&post.tag,
|
||||
&post.timestamp,
|
||||
&post.autosage,
|
||||
&post.deleted_timestamp,
|
||||
&post.bumped,
|
||||
&post.stickied,
|
||||
&post.locked,
|
||||
&post.reviewed,
|
||||
&thread.oldID, &thread.oldBoardID, &thread.oldParentID, &thread.Name, &thread.Tripcode, &thread.Email,
|
||||
&thread.Subject, &thread.Message, &thread.MessageRaw, &thread.Password, &thread.filename,
|
||||
&thread.filenameOriginal, &thread.fileChecksum, &thread.filesize, &thread.imageW, &thread.imageH,
|
||||
&thread.thumbW, &thread.thumbH, &thread.IP, &thread.CreatedOn, &thread.autosage,
|
||||
&thread.bumped, &thread.stickied, &thread.locked,
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
errEv.Err(err).Caller().Msg("Failed to scan thread")
|
||||
return err
|
||||
}
|
||||
_, ok := m.oldBoards[post.boardid]
|
||||
if !ok {
|
||||
// board doesn't exist
|
||||
log.Printf(
|
||||
"Pre-migrated post #%d has an invalid boardid %d (board doesn't exist), skipping",
|
||||
post.id, post.boardid)
|
||||
var foundBoard bool
|
||||
for _, board := range m.boards {
|
||||
if board.oldID == thread.oldBoardID {
|
||||
thread.boardID = board.ID
|
||||
foundBoard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBoard {
|
||||
threadIDsWithInvalidBoards = append(threadIDsWithInvalidBoards, thread.oldID)
|
||||
missingBoardIDs = append(missingBoardIDs, thread.oldBoardID)
|
||||
continue
|
||||
}
|
||||
|
||||
// var stmt *sql.Stmt
|
||||
// var err error
|
||||
preparedStr, _ := gcsql.SetupSQLString(`SELECT id FROM DBPREFIXboards WHERE ui = ?`, m.db)
|
||||
stmt, err := tx.Prepare(preparedStr)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
if err = m.migratePost(tx, &thread, errEv); err != nil {
|
||||
return err
|
||||
}
|
||||
stmt.QueryRow(post.boardid).Scan(&post.newBoardID)
|
||||
|
||||
// gcsql.QueryRowSQL(`SELECT id FROM DBPREFIXboards WHERE uri = ?`, []interface{}{})
|
||||
if post.parentid == 0 {
|
||||
// post is a thread, save it to the DBPREFIXthreads table
|
||||
// []interfaceP{{post.newParentID}
|
||||
|
||||
if err = gcsql.QueryRowSQL(
|
||||
`SELECT board_id FROM DBPREFIXthreads ORDER BY board_id LIMIT 1`,
|
||||
nil,
|
||||
[]interface{}{&post.newBoardID},
|
||||
); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
fmt.Println("Current board ID:", post.newBoardID)
|
||||
prepareStr, _ := gcsql.SetupSQLString(
|
||||
`INSERT INTO DBPREFIXthreads
|
||||
(board_id, locked, stickied)
|
||||
VALUES(?, ?, ?)`, m.db)
|
||||
stmt, err = tx.Prepare(prepareStr)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
stmt.Exec(post.newBoardID, post.locked, post.stickied)
|
||||
// // stmt, err := db.Prepare("INSERT table SET unique_id=? ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id)")
|
||||
// gcsql.ExecSQL(`INSERT INTO DBPREFIXthreads (board_id) VALUES(?)`, post.newBoardID)
|
||||
|
||||
// /*
|
||||
// id
|
||||
// board_id
|
||||
// locked
|
||||
// stickied
|
||||
// anchored
|
||||
// cyclical
|
||||
// last_bump
|
||||
// deleted_at
|
||||
// is_deleted
|
||||
|
||||
// */
|
||||
|
||||
// get and insert replies
|
||||
replyRows, err := m.db.Query(nil, postsQuery+" AND parentid = ?", thread.oldID)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("parentID", thread.oldID).
|
||||
Msg("Failed to get reply rows")
|
||||
return err
|
||||
}
|
||||
m.posts = append(m.posts, post)
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
defer replyRows.Close()
|
||||
|
||||
func (*Pre2021Migrator) migratePostsUtil() error {
|
||||
for replyRows.Next() {
|
||||
var reply migrationPost
|
||||
if err = replyRows.Scan(
|
||||
&reply.oldID, &reply.oldBoardID, &reply.oldParentID, &reply.Name, &reply.Tripcode, &reply.Email,
|
||||
&reply.Subject, &reply.Message, &reply.MessageRaw, &reply.Password, &reply.filename,
|
||||
&reply.filenameOriginal, &reply.fileChecksum, &reply.filesize, &reply.imageW, &reply.imageH,
|
||||
&reply.thumbW, &reply.thumbH, &reply.IP, &reply.CreatedOn, &reply.autosage,
|
||||
&reply.bumped, &reply.stickied, &reply.locked,
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("parentID", thread.oldID).
|
||||
Msg("Failed to scan reply")
|
||||
return err
|
||||
}
|
||||
reply.ThreadID = thread.ThreadID
|
||||
if err = m.migratePost(tx, &reply, errEv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if thread.locked {
|
||||
if _, err = gcsql.Exec(&gcsql.RequestOptions{Tx: tx}, "UPDATE DBPREFIXthreads SET locked = TRUE WHERE id = ?", thread.ThreadID); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Int("threadID", thread.ThreadID).
|
||||
Msg("Unable to re-lock migrated thread")
|
||||
}
|
||||
}
|
||||
migratedThreads++
|
||||
}
|
||||
if err = rows.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to close posts rows")
|
||||
return err
|
||||
}
|
||||
|
||||
if len(threadIDsWithInvalidBoards) > 0 {
|
||||
errEv.Caller().
|
||||
Ints("threadIDs", threadIDsWithInvalidBoards).
|
||||
Ints("boardIDs", missingBoardIDs).
|
||||
Msg("Failed to find boards for threads")
|
||||
return common.NewMigrationError("pre2021", "Found threads with missing boards")
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to commit transaction")
|
||||
return err
|
||||
}
|
||||
gcutil.LogInfo().
|
||||
Int("migratedThreads", migratedThreads).
|
||||
Msg("Migrated threads successfully")
|
||||
return nil
|
||||
}
|
||||
|
|
59
cmd/gochan-migration/internal/pre2021/posts_test.go
Normal file
59
cmd/gochan-migration/internal/pre2021/posts_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigratePosts(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, false)
|
||||
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateBoards()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
var numThreads int
|
||||
if !assert.NoError(t, migrator.db.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXposts WHERE parentid = 0 AND deleted_timestamp IS NULL", nil, []any{&numThreads}), "Failed to get number of threads") {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 2, numThreads, "Expected to have two threads pre-migration")
|
||||
|
||||
if !assert.NoError(t, migrator.MigratePosts()) {
|
||||
t.FailNow()
|
||||
}
|
||||
validatePostMigration(t)
|
||||
}
|
||||
|
||||
func validatePostMigration(t *testing.T) {
|
||||
var numThreads int
|
||||
if !assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXthreads", nil, []any{&numThreads}), "Failed to get number of threads") {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 2, numThreads, "Expected to have two threads pre-migration")
|
||||
|
||||
var numUploadPosts int
|
||||
assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXfiles", nil, []any{&numUploadPosts}))
|
||||
assert.Equal(t, 1, numUploadPosts, "Expected to have 1 upload post")
|
||||
|
||||
var ip string
|
||||
assert.NoError(t, gcsql.QueryRow(nil, "SELECT IP_NTOA FROM DBPREFIXposts WHERE id = 1", nil, []any{&ip}))
|
||||
assert.Equal(t, "192.168.56.1", ip, "Expected to have the correct IP address")
|
||||
|
||||
var numMigratedThreads int
|
||||
if !assert.NoError(t, gcsql.QueryRow(nil, "SELECT COUNT(*) FROM DBPREFIXthreads", nil, []any{&numMigratedThreads}), "Failed to get number of migrated threads") {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 2, numMigratedThreads, "Expected to have three migrated threads")
|
||||
|
||||
var locked bool
|
||||
if !assert.NoError(t, gcsql.QueryRow(nil, "SELECT locked FROM DBPREFIXthreads WHERE id = 1", nil, []any{&locked})) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.True(t, locked, "Expected thread ID 1 to be locked")
|
||||
}
|
|
@ -2,8 +2,11 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
|
@ -12,12 +15,6 @@ import (
|
|||
|
||||
type Pre2021Config struct {
|
||||
config.SQLConfig
|
||||
// DBtype string
|
||||
// DBhost string
|
||||
// DBname string
|
||||
// DBusername string
|
||||
// DBpassword string
|
||||
// DBprefix string
|
||||
DocumentRoot string
|
||||
}
|
||||
|
||||
|
@ -26,9 +23,16 @@ type Pre2021Migrator struct {
|
|||
options *common.MigrationOptions
|
||||
config Pre2021Config
|
||||
|
||||
posts []postTable
|
||||
oldBoards map[int]string // map[boardid]dir
|
||||
newBoards map[int]string // map[board]dir
|
||||
migrationUser *gcsql.Staff
|
||||
boards []migrationBoard
|
||||
sections []migrationSection
|
||||
staff []migrationStaff
|
||||
}
|
||||
|
||||
// IsMigratingInPlace implements common.DBMigrator.
|
||||
func (m *Pre2021Migrator) IsMigratingInPlace() bool {
|
||||
sqlConfig := config.GetSQLConfig()
|
||||
return m.config.DBname == sqlConfig.DBname && m.config.DBhost == sqlConfig.DBhost && m.config.DBprefix == sqlConfig.DBprefix
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) readConfig() error {
|
||||
|
@ -36,84 +40,115 @@ func (m *Pre2021Migrator) readConfig() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.config.SQLConfig = config.GetSQLConfig()
|
||||
return json.Unmarshal(ba, &m.config)
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) Init(options *common.MigrationOptions) error {
|
||||
m.options = options
|
||||
var err error
|
||||
|
||||
if err = m.readConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.config.DBTimeoutSeconds = config.DefaultSQLTimeout
|
||||
m.config.DBMaxOpenConnections = config.DefaultSQLMaxConns
|
||||
m.config.DBMaxIdleConnections = config.DefaultSQLMaxConns
|
||||
m.config.DBConnMaxLifetimeMin = config.DefaultSQLConnMaxLifetimeMin
|
||||
|
||||
m.db, err = gcsql.Open(&m.config.SQLConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) IsMigrated() (bool, error) {
|
||||
var migrated bool
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(m.config.DBTimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
var sqlConfig config.SQLConfig
|
||||
if m.IsMigratingInPlace() {
|
||||
sqlConfig = config.GetSQLConfig()
|
||||
} else {
|
||||
sqlConfig = m.config.SQLConfig
|
||||
}
|
||||
return common.TableExists(ctx, m.db, nil, "DBPREFIXdatabase_version", &sqlConfig)
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) renameTablesForInPlace() error {
|
||||
var err error
|
||||
var query string
|
||||
switch m.config.DBtype {
|
||||
case "mysql":
|
||||
fallthrough
|
||||
case "postgres":
|
||||
query = `SELECT COUNT(*) > 0 FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = ?`
|
||||
default:
|
||||
return false, gcsql.ErrUnsupportedDB
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
if _, err = m.db.Exec(nil, "DROP TABLE DBPREFIXinfo"); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Error dropping info table")
|
||||
return err
|
||||
}
|
||||
if err = m.db.QueryRowSQL(query,
|
||||
[]interface{}{m.config.DBprefix + "migrated", m.config.DBname},
|
||||
[]interface{}{&migrated}); err != nil {
|
||||
return migrated, err
|
||||
for _, table := range renameTables {
|
||||
if _, err = m.db.Exec(nil, fmt.Sprintf(renameTableStatementTemplate, table, table)); err != nil {
|
||||
errEv.Caller().Err(err).
|
||||
Str("table", table).
|
||||
Msg("Error renaming table")
|
||||
return err
|
||||
}
|
||||
}
|
||||
return migrated, err
|
||||
|
||||
if err = gcsql.CheckAndInitializeDatabase(m.config.DBtype, "4"); err != nil {
|
||||
errEv.Caller().Err(err).Msg("Error checking and initializing database")
|
||||
return err
|
||||
}
|
||||
|
||||
if err = m.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Error closing database")
|
||||
return err
|
||||
}
|
||||
m.config.SQLConfig.DBprefix = "_tmp_" + m.config.DBprefix
|
||||
m.db, err = gcsql.Open(&m.config.SQLConfig)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Error reopening database with new prefix")
|
||||
return err
|
||||
}
|
||||
|
||||
common.LogInfo().Msg("Renamed tables for in-place migration")
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) MigrateDB() (bool, error) {
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
migrated, err := m.IsMigrated()
|
||||
if err != nil {
|
||||
errEv.Caller().Err(err).Msg("Error checking if database is migrated")
|
||||
return false, err
|
||||
}
|
||||
if migrated {
|
||||
// db is already migrated, stop
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if m.IsMigratingInPlace() {
|
||||
if err = m.renameTablesForInPlace(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.MigrateBoards(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
// if err = m.MigratePosts(); err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
// if err = m.MigrateStaff("password"); err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
// if err = m.MigrateBans(); err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
// if err = m.MigrateAnnouncements(); err != nil {
|
||||
// return false, err
|
||||
// }
|
||||
common.LogInfo().Msg("Migrated boards successfully")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
if err = m.MigratePosts(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
common.LogInfo().Msg("Migrated threads, posts, and uploads successfully")
|
||||
|
||||
func (*Pre2021Migrator) MigrateStaff(_ string) error {
|
||||
return nil
|
||||
}
|
||||
if err = m.MigrateStaff(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
common.LogInfo().Msg("Migrated staff successfully")
|
||||
|
||||
func (*Pre2021Migrator) MigrateBans() error {
|
||||
return nil
|
||||
}
|
||||
if err = m.MigrateBans(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
common.LogInfo().Msg("Migrated bans and filters successfully")
|
||||
|
||||
func (*Pre2021Migrator) MigrateAnnouncements() error {
|
||||
return nil
|
||||
if err = m.MigrateAnnouncements(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
common.LogInfo().Msg("Migrated staff announcements successfully")
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) Close() error {
|
||||
|
|
127
cmd/gochan-migration/internal/pre2021/pre2021_test.go
Normal file
127
cmd/gochan-migration/internal/pre2021/pre2021_test.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
sqlite3DBDir = "tools/" // relative to gochan project root
|
||||
)
|
||||
|
||||
func setupMigrationTest(t *testing.T, outDir string, migrateInPlace bool) *Pre2021Migrator {
|
||||
dir, err := testutil.GoToGochanRoot(t)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.NoError(t, common.InitTestMigrationLog(t)) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
dbName := "gochan-pre2021.sqlite3db"
|
||||
dbHost := path.Join(dir, sqlite3DBDir, dbName)
|
||||
migratedDBName := "gochan-migrated.sqlite3db"
|
||||
migratedDBHost := path.Join(outDir, migratedDBName)
|
||||
|
||||
if migrateInPlace {
|
||||
oldDbFile, err := os.Open(dbHost)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
defer oldDbFile.Close()
|
||||
|
||||
newDbFile, err := os.OpenFile(migratedDBHost, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
defer newDbFile.Close()
|
||||
|
||||
_, err = io.Copy(newDbFile, oldDbFile)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.NoError(t, oldDbFile.Close())
|
||||
assert.NoError(t, newDbFile.Close())
|
||||
dbHost = migratedDBHost
|
||||
dbName = migratedDBName
|
||||
}
|
||||
|
||||
oldSQLConfig := config.SQLConfig{
|
||||
DBtype: "sqlite3",
|
||||
DBname: dbName,
|
||||
DBhost: dbHost,
|
||||
DBprefix: "gc_",
|
||||
DBusername: "gochan",
|
||||
DBpassword: "password",
|
||||
DBTimeoutSeconds: 600,
|
||||
}
|
||||
migrator := &Pre2021Migrator{
|
||||
config: Pre2021Config{
|
||||
SQLConfig: oldSQLConfig,
|
||||
},
|
||||
}
|
||||
db, err := gcsql.Open(&oldSQLConfig)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
migrator.db = db
|
||||
|
||||
config.SetTestDBConfig("sqlite3", migratedDBHost, migratedDBName, "gochan", "password", "gc_")
|
||||
sqlConfig := config.GetSQLConfig()
|
||||
sqlConfig.DBTimeoutSeconds = 600
|
||||
|
||||
if !assert.NoError(t, gcsql.ConnectToDB(&sqlConfig)) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !migrateInPlace {
|
||||
if !assert.NoError(t, gcsql.CheckAndInitializeDatabase("sqlite3", "4")) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
return migrator
|
||||
}
|
||||
|
||||
func TestPre2021MigrationToNewDB(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, false)
|
||||
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
migrated, err := migrator.MigrateDB()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.False(t, migrated)
|
||||
|
||||
validateBoardMigration(t)
|
||||
validatePostMigration(t)
|
||||
validateBanMigration(t)
|
||||
validateStaffMigration(t)
|
||||
}
|
||||
|
||||
func TestPre2021MigrationInPlace(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, true)
|
||||
if !assert.True(t, migrator.IsMigratingInPlace(), "This test should be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
migrated, err := migrator.MigrateDB()
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.False(t, migrated)
|
||||
|
||||
validateBoardMigration(t)
|
||||
validatePostMigration(t)
|
||||
validateBanMigration(t)
|
||||
validateStaffMigration(t)
|
||||
}
|
|
@ -1,57 +1,33 @@
|
|||
package pre2021
|
||||
|
||||
const (
|
||||
boardsQuery = `SELECT
|
||||
id,
|
||||
dir,
|
||||
type,
|
||||
upload_type,
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
section,
|
||||
max_file_size,
|
||||
max_pages,
|
||||
default_style,
|
||||
locked,
|
||||
anonymous,
|
||||
forced_anon,
|
||||
max_age,
|
||||
autosage_after,
|
||||
no_images_after,
|
||||
max_message_length,
|
||||
embeds_allowed,
|
||||
redirect_to_thread,
|
||||
require_file,
|
||||
enable_catalog
|
||||
FROM DBPREFIXboards`
|
||||
sectionsQuery = "SELECT id, list_order, hidden, name, abbreviation FROM DBPREFIXsections"
|
||||
|
||||
postsQuery = `SELECT
|
||||
id,
|
||||
boardid,
|
||||
parentid,
|
||||
name,
|
||||
tripcode,
|
||||
email,
|
||||
subject,
|
||||
message,
|
||||
message_raw,
|
||||
password,
|
||||
filename,
|
||||
filename_original,
|
||||
file_checksum,
|
||||
filesize,
|
||||
image_w,
|
||||
image_h,
|
||||
thumb_w,
|
||||
thumb_h,
|
||||
ip,
|
||||
tag,
|
||||
timestamp,
|
||||
autosage,
|
||||
deleted_timestamp,
|
||||
bumped,
|
||||
stickied,
|
||||
locked,
|
||||
reviewed from DBPREFIXposts WHERE deleted_timestamp = NULL`
|
||||
boardsQuery = `SELECT id, list_order, dir, title, subtitle, description, section, max_file_size, max_pages,
|
||||
default_style, locked, created_on, anonymous, forced_anon, autosage_after, no_images_after, max_message_length, embeds_allowed,
|
||||
redirect_to_thread, require_file, enable_catalog
|
||||
FROM DBPREFIXboards`
|
||||
|
||||
postsQuery = `SELECT id, boardid, parentid, name, tripcode, email, subject, message, message_raw, password, filename,
|
||||
filename_original, file_checksum, filesize, image_w, image_h, thumb_w, thumb_h, ip, timestamp, autosage,
|
||||
bumped, stickied, locked FROM DBPREFIXposts WHERE deleted_timestamp IS NULL`
|
||||
|
||||
threadsQuery = postsQuery + " AND parentid = 0"
|
||||
|
||||
staffQuery = `SELECT id, username, rank, boards, added_on, last_active FROM DBPREFIXstaff`
|
||||
|
||||
bansQuery = `SELECT id, allow_read, COALESCE(ip, '') as ip, name, name_is_regex, filename, file_checksum, boards, staff,
|
||||
timestamp, expires, permaban, reason, type, staff_note, appeal_at, can_appeal FROM DBPREFIXbanlist`
|
||||
|
||||
announcementsQuery = "SELECT id, subject, message, poster, timestamp FROM DBPREFIXannouncements"
|
||||
|
||||
renameTableStatementTemplate = "ALTER TABLE %s RENAME TO _tmp_%s"
|
||||
)
|
||||
|
||||
var (
|
||||
// tables to be renamed to _tmp_DBPREFIX* to work around SQLite's lack of support for changing/removing columns
|
||||
renameTables = []string{
|
||||
"DBPREFIXannouncements", "DBPREFIXappeals", "DBPREFIXbanlist", "DBPREFIXboards", "DBPREFIXembeds", "DBPREFIXlinks",
|
||||
"DBPREFIXposts", "DBPREFIXreports", "DBPREFIXsections", "DBPREFIXsessions", "DBPREFIXstaff", "DBPREFIXwordfilters",
|
||||
}
|
||||
)
|
||||
|
|
121
cmd/gochan-migration/internal/pre2021/staff.go
Normal file
121
cmd/gochan-migration/internal/pre2021/staff.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gochan-org/gochan/cmd/gochan-migration/internal/common"
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/gochan-org/gochan/pkg/gcutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type migrationStaff struct {
|
||||
gcsql.Staff
|
||||
boards string
|
||||
oldID int
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) getMigrationUser(errEv *zerolog.Event) (*gcsql.Staff, error) {
|
||||
if m.migrationUser != nil {
|
||||
return m.migrationUser, nil
|
||||
}
|
||||
|
||||
user := &gcsql.Staff{
|
||||
Username: "pre2021-migration" + gcutil.RandomString(8),
|
||||
AddedOn: time.Now(),
|
||||
}
|
||||
_, err := gcsql.Exec(nil, "INSERT INTO DBPREFIXstaff(username,password_checksum,global_rank,is_active) values(?,'',0,0)", user.Username)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Str("username", user.Username).Msg("Failed to create migration user")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = gcsql.QueryRow(nil, "SELECT id FROM DBPREFIXstaff WHERE username = ?", []any{user.Username}, []any{&user.ID}); err != nil {
|
||||
errEv.Err(err).Caller().Str("username", user.Username).Msg("Failed to get migration user ID")
|
||||
return nil, err
|
||||
}
|
||||
m.migrationUser = user
|
||||
m.staff = append(m.staff, migrationStaff{Staff: *user})
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m *Pre2021Migrator) MigrateStaff() error {
|
||||
errEv := common.LogError()
|
||||
defer errEv.Discard()
|
||||
|
||||
_, err := m.getMigrationUser(errEv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := m.db.Query(nil, staffQuery)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to get ban rows")
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var staff migrationStaff
|
||||
if err = rows.Scan(&staff.oldID, &staff.Username, &staff.Rank, &staff.boards, &staff.AddedOn, &staff.LastLogin); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to scan staff row")
|
||||
return err
|
||||
}
|
||||
m.staff = append(m.staff, staff)
|
||||
}
|
||||
for _, staff := range m.staff {
|
||||
newStaff, err := gcsql.GetStaffByUsername(staff.Username, false)
|
||||
if err == nil {
|
||||
// found staff
|
||||
gcutil.LogInfo().Str("username", staff.Username).Int("rank", staff.Rank).Msg("Found matching staff account")
|
||||
staff.ID = newStaff.ID
|
||||
} else if errors.Is(err, gcsql.ErrUnrecognizedUsername) {
|
||||
// staff doesn't exist, create it (with invalid checksum to be updated by the admin)
|
||||
if _, err := gcsql.Exec(nil,
|
||||
"INSERT INTO DBPREFIXstaff(username,password_checksum,global_rank,added_on,last_login,is_active) values(?,'',?,?,?,1)",
|
||||
staff.Username, staff.Rank, staff.AddedOn, staff.LastLogin,
|
||||
); err != nil {
|
||||
errEv.Err(err).Caller().Str("username", staff.Username).Int("rank", staff.Rank).Msg("Failed to migrate staff account")
|
||||
return err
|
||||
}
|
||||
if staff.ID, err = gcsql.GetStaffID(staff.Username); err != nil {
|
||||
errEv.Err(err).Caller().Str("username", staff.Username).Msg("Failed to get staff account ID")
|
||||
return err
|
||||
}
|
||||
gcutil.LogInfo().Str("username", staff.Username).Int("rank", staff.Rank).Msg("Successfully migrated staff account")
|
||||
} else {
|
||||
errEv.Err(err).Caller().Str("username", staff.Username).Msg("Failed to get staff account info")
|
||||
return err
|
||||
}
|
||||
|
||||
if staff.boards != "" && staff.boards != "*" {
|
||||
boardsArr := strings.Split(staff.boards, ",")
|
||||
for _, board := range boardsArr {
|
||||
board = strings.TrimSpace(board)
|
||||
boardID, err := gcsql.GetBoardIDFromDir(board)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("username", staff.Username).
|
||||
Str("board", board).
|
||||
Msg("Failed to get board ID")
|
||||
return err
|
||||
}
|
||||
if _, err = gcsql.Exec(nil, "INSERT INTO DBPREFIXboard_staff(board_id,staff_id) VALUES(?,?)", boardID, staff.ID); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("username", staff.Username).
|
||||
Str("board", board).
|
||||
Msg("Failed to apply staff board info")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = rows.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed to close staff rows")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
33
cmd/gochan-migration/internal/pre2021/staff_test.go
Normal file
33
cmd/gochan-migration/internal/pre2021/staff_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package pre2021
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/gcsql"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigrateStaff(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
migrator := setupMigrationTest(t, outDir, false)
|
||||
if !assert.False(t, migrator.IsMigratingInPlace(), "This test should not be migrating in place") {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateBoards()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.NoError(t, migrator.MigrateStaff()) {
|
||||
t.FailNow()
|
||||
}
|
||||
validateStaffMigration(t)
|
||||
}
|
||||
|
||||
func validateStaffMigration(t *testing.T) {
|
||||
migratedAdmin, err := gcsql.GetStaffByUsername("migratedadmin", true)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
assert.Equal(t, 3, migratedAdmin.Rank)
|
||||
}
|
|
@ -56,9 +56,14 @@ func main() {
|
|||
fatalEv.Discard()
|
||||
}()
|
||||
|
||||
if !updateDB && (options.ChanType == "" || options.OldChanConfig == "") {
|
||||
flag.PrintDefaults()
|
||||
fatalEv.Msg("Missing required oldchan value")
|
||||
if !updateDB {
|
||||
if options.ChanType == "" {
|
||||
flag.PrintDefaults()
|
||||
fatalEv.Msg("Missing required oldchan value")
|
||||
} else if options.OldChanConfig == "" {
|
||||
flag.PrintDefaults()
|
||||
fatalEv.Msg("Missing required oldconfig value")
|
||||
}
|
||||
} else if updateDB {
|
||||
options.ChanType = "gcupdate"
|
||||
}
|
||||
|
@ -82,9 +87,22 @@ func main() {
|
|||
default:
|
||||
fatalEv.Msg("Unsupported chan type, Currently only pre2021 database migration is supported")
|
||||
}
|
||||
migratingInPlace := migrator.IsMigratingInPlace()
|
||||
common.LogInfo().
|
||||
Str("oldChanType", options.ChanType).
|
||||
Str("oldChanConfig", options.OldChanConfig).
|
||||
Bool("migratingInPlace", migratingInPlace).
|
||||
Msg("Starting database migration")
|
||||
|
||||
config.InitConfig(versionStr)
|
||||
if !updateDB {
|
||||
sqlCfg := config.GetSQLConfig()
|
||||
sqlCfg := config.GetSQLConfig()
|
||||
if migratingInPlace && sqlCfg.DBtype == "sqlite3" && !updateDB {
|
||||
common.LogWarning().
|
||||
Str("dbType", sqlCfg.DBtype).
|
||||
Bool("migrateInPlace", migratingInPlace).
|
||||
Msg("SQLite has limitations with table column changes")
|
||||
}
|
||||
if !migratingInPlace {
|
||||
err = gcsql.ConnectToDB(&sqlCfg)
|
||||
if err != nil {
|
||||
fatalEv.Err(err).Caller().Msg("Failed to connect to the database")
|
||||
|
@ -103,7 +121,7 @@ func main() {
|
|||
fatalEv.Msg("Unable to migrate database")
|
||||
}
|
||||
if migrated {
|
||||
common.LogInfo().Msg("Database is already migrated")
|
||||
common.LogWarning().Msg("Database is already migrated")
|
||||
} else {
|
||||
common.LogInfo().Str("chanType", options.ChanType).Msg("Database migration complete")
|
||||
}
|
||||
|
|
|
@ -118,23 +118,26 @@ func getAllPostsToDelete(postIDs []any, fileOnly bool) ([]delPost, []any, error)
|
|||
params := postIDs
|
||||
if fileOnly {
|
||||
// only deleting this post's file, not subfiles if it's an OP
|
||||
query = "SELECT * FROM DBPREFIXv_posts_to_delete_file_only WHERE postid IN " + setPart
|
||||
query = "SELECT post_id, thread_id, op_id, is_top_post, filename, dir FROM DBPREFIXv_posts_to_delete_file_only WHERE post_id IN " + setPart
|
||||
} else {
|
||||
// deleting everything, including subfiles
|
||||
params = append(params, postIDs...)
|
||||
query = "SELECT * FROM DBPREFIXv_posts_to_delete WHERE postid IN " + setPart +
|
||||
` OR thread_id IN (SELECT thread_id from DBPREFIXposts op WHERE opid IN ` + setPart + ` AND is_top_post)`
|
||||
query = "SELECT post_id, thread_id, op_id, is_top_post, filename, dir FROM DBPREFIXv_posts_to_delete WHERE post_id IN " +
|
||||
setPart + " OR thread_id IN (SELECT thread_id from DBPREFIXposts op WHERE op_id IN " + setPart + " AND is_top_post)"
|
||||
}
|
||||
rows, err := gcsql.QuerySQL(query, params...)
|
||||
rows, cancel, err := gcsql.QueryTimeoutSQL(nil, query, params...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer func() {
|
||||
rows.Close()
|
||||
cancel()
|
||||
}()
|
||||
var posts []delPost
|
||||
var postIDsAny []any
|
||||
for rows.Next() {
|
||||
var post delPost
|
||||
if err = rows.Scan(&post.postID, &post.threadID, &post.opID, &post.isOP, &post.filename, &post.boardDir); err != nil {
|
||||
rows.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
posts = append(posts, post)
|
||||
|
@ -258,7 +261,7 @@ func validatePostPasswords(posts []any, passwordMD5 string) (bool, error) {
|
|||
params = append(params, posts[p])
|
||||
}
|
||||
|
||||
err := gcsql.QueryRowSQL(queryPosts, params, []any{&count})
|
||||
err := gcsql.QueryRow(nil, queryPosts, params, []any{&count})
|
||||
return count == len(posts), err
|
||||
}
|
||||
|
||||
|
@ -281,21 +284,24 @@ func markPostsAsDeleted(posts []any, request *http.Request, writer http.Response
|
|||
defer cancel()
|
||||
|
||||
tx, err := gcsql.BeginContextTx(ctx)
|
||||
opts := &gcsql.RequestOptions{Context: ctx, Tx: tx, Cancel: cancel}
|
||||
|
||||
wantsJSON := serverutil.IsRequestingJSON(request)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Unable to start deletion transaction")
|
||||
serveError(writer, "Unable to delete posts", http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
|
||||
return false
|
||||
}
|
||||
defer tx.Rollback()
|
||||
const postsError = "Unable to mark post(s) as deleted"
|
||||
const threadsError = "Unable to mark thread(s) as deleted"
|
||||
if _, err = gcsql.ExecTxSQL(tx, deletePostsSQL, posts...); err != nil {
|
||||
const postsError = "Unable to delete post(s)"
|
||||
const threadsError = "Unable to delete thread(s)"
|
||||
if _, err = gcsql.Exec(opts, deletePostsSQL, posts...); err != nil {
|
||||
serveError(writer, postsError, http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
|
||||
return false
|
||||
}
|
||||
|
||||
if _, err = gcsql.ExecTxSQL(tx, deleteThreadSQL, posts...); err != nil {
|
||||
if _, err = gcsql.Exec(opts, deleteThreadSQL, posts...); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Unable to mark thread(s) as deleted")
|
||||
serveError(writer, threadsError, http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
|
||||
return false
|
||||
}
|
||||
|
@ -351,7 +357,7 @@ func deletePostFiles(posts []delPost, deleteIDs []any, permDelete bool, request
|
|||
http.StatusInternalServerError, wantsJSON, errEv.Array("errors", errArr))
|
||||
return false
|
||||
}
|
||||
_, err = gcsql.ExecSQL(deleteFilesSQL, deleteIDs...)
|
||||
_, err = gcsql.ExecTimeoutSQL(nil, deleteFilesSQL, deleteIDs...)
|
||||
if err != nil {
|
||||
serveError(writer, "Unable to delete file entries from database",
|
||||
http.StatusInternalServerError, wantsJSON, errEv.Err(err).Caller())
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gochan-org/gochan/pkg/building"
|
||||
"github.com/gochan-org/gochan/pkg/config"
|
||||
|
@ -29,16 +30,16 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
if editBtn == "Edit post" {
|
||||
var err error
|
||||
if len(checkedPosts) == 0 {
|
||||
server.ServeErrorPage(writer, "You need to select one post to edit.")
|
||||
server.ServeError(writer, server.NewServerError("You need to select one post to edit", http.StatusBadRequest), wantsJSON, nil)
|
||||
return
|
||||
} else if len(checkedPosts) > 1 {
|
||||
server.ServeErrorPage(writer, "You can only edit one post at a time.")
|
||||
server.ServeError(writer, server.NewServerError("You can only edit one post at a time", http.StatusBadRequest), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
rank := manage.GetStaffRank(request)
|
||||
if password == "" && rank == 0 {
|
||||
server.ServeErrorPage(writer, "Password required for post editing")
|
||||
server.ServeError(writer, server.NewServerError("Password required for post editing", http.StatusUnauthorized), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
passwordMD5 := gcutil.Md5Sum(password)
|
||||
|
@ -47,30 +48,36 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Error getting post information")
|
||||
server.ServeError(writer, server.NewServerError("Error getting post information", http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
errEv.Int("postID", post.ID)
|
||||
|
||||
if post.Password != passwordMD5 && rank == 0 {
|
||||
server.ServeErrorPage(writer, "Wrong password")
|
||||
server.ServeError(writer, server.NewServerError("Wrong password", http.StatusUnauthorized), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
board, err := post.GetBoard()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Unable to get board ID from post")
|
||||
server.ServeErrorPage(writer, "Unable to get board ID from post: "+err.Error())
|
||||
server.ServeError(writer, server.NewServerError("Unable to get board ID from post", http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
if strings.Contains(string(post.Message), `<span class="dice-roll">`) && !config.GetBoardConfig(board.Dir).AllowDiceRerolls {
|
||||
server.ServeError(writer, server.NewServerError("Unable to edit post, dice rerolls are not allowed on this board", http.StatusForbidden), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
errEv.Str("board", board.Dir)
|
||||
upload, err := post.GetUpload()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
server.ServeErrorPage(writer, "Error getting post upload info: "+err.Error())
|
||||
server.ServeError(writer, server.NewServerError("Error getting post upload info: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
data := map[string]any{
|
||||
"boards": gcsql.AllBoards,
|
||||
"systemCritical": config.GetSystemCriticalConfig(),
|
||||
"siteConfig": config.GetSiteConfig(),
|
||||
|
@ -87,7 +94,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
if err = serverutil.MinifyTemplate(gctemplates.PostEdit, data, &buf, "text/html"); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Error executing edit post template")
|
||||
server.ServeError(writer, "Error executing edit post template: "+err.Error(), wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Error executing edit post template: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
writer.Write(buf.Bytes())
|
||||
|
@ -101,7 +108,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
errEv.Err(err).Caller().
|
||||
Str("postid", postIDstr).
|
||||
Msg("Invalid form data")
|
||||
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, map[string]any{
|
||||
"postid": postid,
|
||||
})
|
||||
return
|
||||
|
@ -110,7 +117,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
post, err := gcsql.GetPostFromID(postid, true)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Unable to find post")
|
||||
server.ServeError(writer, "Unable to find post", wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, server.NewServerError("Unable to find post", http.StatusBadRequest), wantsJSON, map[string]any{
|
||||
"postid": postid,
|
||||
})
|
||||
return
|
||||
|
@ -121,7 +128,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
errEv.Err(err).Caller().
|
||||
Str("boardID", boardIDstr).
|
||||
Msg("Invalid form data")
|
||||
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
gcutil.LogInt("boardID", boardid, infoEv, errEv)
|
||||
|
@ -129,13 +136,13 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
rank := manage.GetStaffRank(request)
|
||||
passwordMD5 := gcutil.Md5Sum(password)
|
||||
if post.Password != passwordMD5 && rank == 0 {
|
||||
server.ServeError(writer, "Wrong password", wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Wrong password", http.StatusUnauthorized), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
board, err := gcsql.GetBoardFromID(boardid)
|
||||
if err != nil {
|
||||
server.ServeError(writer, "Invalid form data: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, server.NewServerError("Invalid form data: "+err.Error(), http.StatusBadRequest), wantsJSON, map[string]any{
|
||||
"boardid": boardid,
|
||||
})
|
||||
errEv.Err(err).Caller().Msg("Invalid form data")
|
||||
|
@ -145,18 +152,18 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
if doEdit == "upload" {
|
||||
oldUpload, err := post.GetUpload()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
server.ServeError(writer, err.Error(), wantsJSON, nil)
|
||||
errEv.Err(err).Caller().Msg("Unable to get post upload")
|
||||
server.ServeError(writer, server.NewServerError("Error getting post upload info: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
|
||||
upload, err := uploads.AttachUploadFromRequest(request, writer, post, board, gcutil.LogInfo(), errEv)
|
||||
if err != nil {
|
||||
server.ServeError(writer, err.Error(), wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Unable to attach upload:"+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
if upload == nil {
|
||||
server.ServeError(writer, "Missing upload replacement", wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Missing upload replacement", http.StatusBadRequest), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
documentRoot := config.GetSystemCriticalConfig().DocumentRoot
|
||||
|
@ -167,7 +174,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
path.Join(documentRoot, board.Dir, "thumb", oldUpload.Filename))
|
||||
if err = post.UnlinkUploads(false); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
server.ServeError(writer, "Error unlinking old upload from post: "+err.Error(), wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Error unlinking old upload from post: "+err.Error(), http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
if oldUpload.Filename != "deleted" {
|
||||
|
@ -184,7 +191,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
Str("newFilename", upload.Filename).
|
||||
Str("newOriginalFilename", upload.OriginalFilename).
|
||||
Send()
|
||||
server.ServeError(writer, "Error attaching new upload: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, server.NewServerError("Error attaching new upload: "+err.Error(), http.StatusInternalServerError), wantsJSON, map[string]any{
|
||||
"filename": upload.OriginalFilename,
|
||||
})
|
||||
filePath = path.Join(documentRoot, board.Dir, "src", upload.Filename)
|
||||
|
@ -200,17 +207,15 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
var recovered bool
|
||||
_, err, recovered = events.TriggerEvent("message-pre-format", post, request)
|
||||
if recovered {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Recovered from a panic in an event handler (message-pre-format)", wantsJSON, map[string]interface{}{
|
||||
"postid": post.ID,
|
||||
})
|
||||
server.ServeError(writer, server.NewServerError("Recovered from a panic in an event handler (message-pre-format)",
|
||||
http.StatusInternalServerError), wantsJSON, map[string]any{"postid": post.ID})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("triggeredEvent", "message-pre-format").
|
||||
Send()
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, server.NewServerError(err.Error(), http.StatusInternalServerError), wantsJSON, map[string]any{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
|
@ -219,8 +224,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
// trigger the pre-format event
|
||||
_, err, recovered := events.TriggerEvent("message-pre-format", post, request)
|
||||
if recovered {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Recovered from a panic in an event handler (message-pre-format)", wantsJSON, nil)
|
||||
server.ServeError(writer, server.NewServerError("Recovered from a panic in an event handler (message-pre-format)", http.StatusInternalServerError), wantsJSON, nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -259,7 +263,7 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Unable to edit post")
|
||||
server.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Unable to edit post: "+err.Error(), wantsJSON, map[string]any{
|
||||
"postid": post.ID,
|
||||
})
|
||||
return
|
||||
|
@ -285,9 +289,9 @@ func editPost(checkedPosts []int, editBtn string, doEdit string, writer http.Res
|
|||
}
|
||||
|
||||
if err = building.BuildBoards(false, boardid); err != nil {
|
||||
server.ServeErrorPage(writer, "Error rebuilding boards: "+err.Error())
|
||||
server.ServeError(writer, "Error rebuilding boards: "+err.Error(), wantsJSON, nil)
|
||||
} else if err = building.BuildFrontPage(); err != nil {
|
||||
server.ServeErrorPage(writer, "Error rebuilding front page: "+err.Error())
|
||||
server.ServeError(writer, "Error rebuilding front page: "+err.Error(), wantsJSON, nil)
|
||||
} else {
|
||||
http.Redirect(writer, request, post.WebPath(), http.StatusFound)
|
||||
infoEv.Msg("Post edited")
|
||||
|
|
|
@ -78,7 +78,7 @@ func main() {
|
|||
gcutil.LogFatal().Err(err).Msg("Failed to initialize the database")
|
||||
}
|
||||
events.TriggerEvent("db-initialized")
|
||||
events.RegisterEvent([]string{"db-views-reset"}, func(trigger string, i ...interface{}) error {
|
||||
events.RegisterEvent([]string{"db-views-reset"}, func(_ string, _ ...any) error {
|
||||
gcutil.LogInfo().Msg("SQL views reset")
|
||||
return nil
|
||||
})
|
||||
|
|
|
@ -59,7 +59,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
return
|
||||
}
|
||||
if !post.IsTopPost {
|
||||
server.ServeError(writer, "You appear to be trying to move a post that is not the top post in the thread", wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "You appear to be trying to move a post that is not the top post in the thread", wantsJSON, map[string]any{
|
||||
"postid": checkedPosts[0],
|
||||
})
|
||||
return
|
||||
|
@ -69,7 +69,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("srcBoardIDstr", request.PostFormValue("boardid")).Send()
|
||||
server.ServeError(writer, fmt.Sprintf("Invalid or missing boarid: %q", request.PostFormValue("boardid")), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, fmt.Sprintf("Invalid or missing boarid: %q", request.PostFormValue("boardid")), wantsJSON, map[string]any{
|
||||
"boardid": srcBoardID,
|
||||
})
|
||||
return
|
||||
|
@ -85,7 +85,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
}
|
||||
gcutil.LogStr("srcBoard", srcBoard.Dir, errEv, infoEv)
|
||||
buf := bytes.NewBufferString("")
|
||||
if err = serverutil.MinifyTemplate(gctemplates.MoveThreadPage, map[string]interface{}{
|
||||
if err = serverutil.MinifyTemplate(gctemplates.MoveThreadPage, map[string]any{
|
||||
"boardConfig": config.GetBoardConfig(srcBoard.Dir),
|
||||
"postid": post.ID,
|
||||
"destBoards": destBoards,
|
||||
|
@ -106,7 +106,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Str("postIDstr", postIDstr).Send()
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
server.ServeError(writer, fmt.Sprintf("Error parsing postid value: %q: %s", postIDstr, err.Error()), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, fmt.Sprintf("Error parsing postid value: %q: %s", postIDstr, err.Error()), wantsJSON, map[string]any{
|
||||
"postid": postIDstr,
|
||||
})
|
||||
return
|
||||
|
@ -119,7 +119,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Str("srcBoardIDstr", srcBoardIDstr).Send()
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
server.ServeError(writer, fmt.Sprintf("Error parsing srcboardid value: %q: %s", srcBoardIDstr, err.Error()), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, fmt.Sprintf("Error parsing srcboardid value: %q: %s", srcBoardIDstr, err.Error()), wantsJSON, map[string]any{
|
||||
"srcboardid": srcBoardIDstr,
|
||||
})
|
||||
return
|
||||
|
@ -129,7 +129,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Int("srcBoardID", srcBoardID).Send()
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
|
||||
"srcboardid": srcBoardID,
|
||||
})
|
||||
return
|
||||
|
@ -142,7 +142,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Str("destBoardIDstr", destBoardIDstr).Send()
|
||||
writer.WriteHeader(http.StatusBadRequest)
|
||||
server.ServeError(writer, fmt.Sprintf("Error parsing destboardid value: %q: %s", destBoardIDstr, err.Error()), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, fmt.Sprintf("Error parsing destboardid value: %q: %s", destBoardIDstr, err.Error()), wantsJSON, map[string]any{
|
||||
"destboardid": destBoardIDstr,
|
||||
})
|
||||
return
|
||||
|
@ -153,7 +153,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Int("destBoardID", destBoardID).Send()
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
|
||||
"destboardid": destBoardID,
|
||||
})
|
||||
return
|
||||
|
@ -164,7 +164,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
|
||||
"postid": postID,
|
||||
})
|
||||
return
|
||||
|
@ -178,7 +178,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
|
||||
if err = post.ChangeBoardID(destBoardID); err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed changing thread board ID")
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, err.Error(), wantsJSON, map[string]any{
|
||||
"postID": postID,
|
||||
"destBoardID": destBoardID,
|
||||
})
|
||||
|
@ -189,7 +189,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Unable to get upload info")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Error getting list of files in thread", wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Error getting list of files in thread", wantsJSON, map[string]any{
|
||||
"postid": post.ID,
|
||||
})
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
if err != nil {
|
||||
// got at least one error while trying to move files (if there were any)
|
||||
server.ServeError(writer, "Error while moving post upload: "+err.Error(), wantsJSON,
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"postID": postID,
|
||||
"srcBoard": srcBoard.Dir,
|
||||
"destBoard": destBoard.Dir,
|
||||
|
@ -258,7 +258,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Msg("Failed deleting thread page")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Failed deleting thread page: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Failed deleting thread page: "+err.Error(), wantsJSON, map[string]any{
|
||||
"postID": postID,
|
||||
"srcBoard": srcBoard.Dir,
|
||||
})
|
||||
|
@ -269,7 +269,7 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
errEv.Err(err).Caller().
|
||||
Msg("Failed deleting thread JSON file")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Failed deleting thread JSON file: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Failed deleting thread JSON file: "+err.Error(), wantsJSON, map[string]any{
|
||||
"postID": postID,
|
||||
"srcBoard": srcBoard.Dir,
|
||||
})
|
||||
|
@ -278,27 +278,27 @@ func moveThread(checkedPosts []int, moveBtn string, doMove string, writer http.R
|
|||
|
||||
if err = building.BuildThreadPages(post); err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Failed building thread page: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Failed building thread page: "+err.Error(), wantsJSON, map[string]any{
|
||||
"postid": postID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = building.BuildBoardPages(srcBoard, errEv); err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Failed building board page: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Failed building board page: "+err.Error(), wantsJSON, map[string]any{
|
||||
"srcBoardID": srcBoardID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if err = building.BuildBoardPages(destBoard, errEv); err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
server.ServeError(writer, "Failed building destination board page: "+err.Error(), wantsJSON, map[string]interface{}{
|
||||
server.ServeError(writer, "Failed building destination board page: "+err.Error(), wantsJSON, map[string]any{
|
||||
"destBoardID": destBoardID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if wantsJSON {
|
||||
server.ServeJSON(writer, map[string]interface{}{
|
||||
server.ServeJSON(writer, map[string]any{
|
||||
"status": "success",
|
||||
"postID": postID,
|
||||
"srcBoard": srcBoard.Dir,
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"DBprefix": "gc_",
|
||||
"_DBprefix_info": "The prefix automataically applied to tables when the database is being provisioned and queried",
|
||||
|
||||
"CheckRequestReferer": true,
|
||||
"Lockdown": false,
|
||||
"LockdownMessage": "This imageboard has temporarily disabled posting. We apologize for the inconvenience",
|
||||
"Modboard": "staff",
|
||||
|
@ -91,12 +92,17 @@
|
|||
"somemod:blue"
|
||||
],
|
||||
"BanMessage": "USER WAS BANNED FOR THIS POST",
|
||||
|
||||
"EnableCyclicThreads": true,
|
||||
"CyclicThreadNumPosts": 500,
|
||||
"EnableEmbeds": true,
|
||||
"EnableNoFlag": true,
|
||||
"EmbedWidth": 200,
|
||||
"EmbedHeight": 164,
|
||||
"ImagesOpenNewTab": true,
|
||||
"NewTabOnOutlinks": true,
|
||||
"DisableBBcode": false,
|
||||
"AllowDiceRerolls": false,
|
||||
|
||||
"MinifyHTML": true,
|
||||
"MinifyJS": true,
|
||||
|
@ -108,8 +114,8 @@
|
|||
"SiteKey": "your site key goes here (if you want a captcha, make sure to replace '_Captcha' with 'Captcha'",
|
||||
"AccountSecret": "your account secret key goes here"
|
||||
},
|
||||
"GeoIPType": "mmdb",
|
||||
"GeoIPOptions": {
|
||||
"_GeoIPType": "mmdb",
|
||||
"_GeoIPOptions": {
|
||||
"dbLocation": "/usr/share/geoip/GeoIP2.mmdb",
|
||||
"isoCode": "en"
|
||||
},
|
||||
|
|
5
examples/plugins/bbcode.lua
Normal file
5
examples/plugins/bbcode.lua
Normal file
|
@ -0,0 +1,5 @@
|
|||
local bbcode = require("bbcode")
|
||||
|
||||
bbcode.set_tag("rcv", function(node)
|
||||
return {name="span", attrs={class="rcv"}}
|
||||
end)
|
6386
frontend/package-lock.json
generated
6386
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "gochan.js",
|
||||
"version": "4.0.1",
|
||||
"version": "4.1.0",
|
||||
"description": "",
|
||||
"main": "./ts/main.ts",
|
||||
"private": true,
|
||||
|
@ -19,28 +19,28 @@
|
|||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"jquery": "^3.7.1",
|
||||
"jquery-ui": "^1.13.2",
|
||||
"jquery-ui": "^1.14.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"webstorage-polyfill": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@jest/globals": "^29.5.0",
|
||||
"@types/jquery": "^3.5.29",
|
||||
"@types/jqueryui": "^1.12.21",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"eslint": "^8.55.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-config": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"sass": "^1.69.5",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"@babel/preset-env": "^7.26.9",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/jqueryui": "^1.12.23",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.24.1",
|
||||
"eslint": "^9.20.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-config": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"sass": "^1.85.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,4 +18,16 @@
|
|||
-ms-border-radius: $properties;
|
||||
-webkit-border-radius: $properties;
|
||||
border-radius: $properties;
|
||||
}
|
||||
|
||||
@mixin upload-box($bg, $a, $a-visited) {
|
||||
div#upload-box {
|
||||
background: $bg;
|
||||
a {
|
||||
color: $a;
|
||||
}
|
||||
a:hover, a:target, a:focus {
|
||||
color: $a-visited;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
@use 'util';
|
||||
|
||||
@mixin yotsuba(
|
||||
$fadepath,
|
||||
|
@ -101,4 +101,10 @@
|
|||
color: $subjectcol;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #222;
|
||||
}
|
||||
|
||||
@include util.upload-box(#aaa, #444, #666);
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
@import 'bunkerchan/colors';
|
||||
@import 'bunkerchan/front';
|
||||
@import 'bunkerchan/img';
|
||||
@import 'util';
|
||||
@use 'bunkerchan/colors';
|
||||
@use 'bunkerchan/front';
|
||||
@use 'bunkerchan/img';
|
||||
@use 'util';
|
||||
|
||||
body {
|
||||
background: $bgcol;
|
||||
color: $color;
|
||||
font-family: $font-family;
|
||||
background: colors.$bgcol;
|
||||
color: colors.$color;
|
||||
font-family: colors.$font-family;
|
||||
font-size: 70%;
|
||||
}
|
||||
|
||||
|
@ -19,20 +19,30 @@ hr {
|
|||
}
|
||||
|
||||
a, a:visited {
|
||||
color: $topborder;
|
||||
color: colors.$topborder;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div#staff, select.post-actions {
|
||||
background: $topbarbg;
|
||||
border: 1px solid $topborder;
|
||||
background: colors.$topbarbg;
|
||||
border: 1px solid colors.$topborder;
|
||||
}
|
||||
|
||||
div#topbar,
|
||||
div#topbar a,
|
||||
div#topbar a:visited {
|
||||
background: $topbarbg;
|
||||
border-bottom: 1px solid $topborder;
|
||||
div#topbar a:visited,
|
||||
div.dropdown-menu {
|
||||
background: colors.$topbarbg;
|
||||
border-bottom: 1px solid colors.$topborder;
|
||||
}
|
||||
|
||||
div.dropdown-menu {
|
||||
border-left: 1px solid colors.$topborder;
|
||||
border-right: 1px solid colors.$topborder;
|
||||
}
|
||||
|
||||
div.dropdown-menu a:hover, div.dropdown-menu a:active {
|
||||
background: colors.$bgcol;
|
||||
}
|
||||
|
||||
table#pages, table#pages * {
|
||||
|
|
|
@ -2,6 +2,7 @@ $bgcol: #1D1F21;
|
|||
$color: #ACACAC;
|
||||
$hcol: #663E11;
|
||||
$inputbg: #282A2E;
|
||||
$inputbg2: #16171A;
|
||||
$topborder: #B0790A;
|
||||
$linkcol: #FFB300;
|
||||
$bordercol: #117743;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@import 'colors';
|
||||
@use 'colors';
|
||||
|
||||
div.section-title-block {
|
||||
background: $topbarbg;
|
||||
border: 1px solid $topborder;
|
||||
background: colors.$topbarbg;
|
||||
border: 1px solid colors.$topborder;
|
||||
border-radius: 5px 5px 0px 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
@import 'colors';
|
||||
@use 'colors';
|
||||
|
||||
%formstyle {
|
||||
background: $inputbg;
|
||||
border: 1px double $inputborder;
|
||||
color: $color;
|
||||
background: colors.$inputbg;
|
||||
border: 1px double colors.$inputborder;
|
||||
color: colors.$color;
|
||||
}
|
||||
|
||||
%darkselect {
|
||||
background: colors.$inputbg2;
|
||||
border-radius: 5px;
|
||||
color: colors.$color;
|
||||
@extend %formstyle;
|
||||
}
|
||||
|
||||
div.reply,
|
||||
th.postblock,
|
||||
|
@ -17,20 +23,25 @@ table.mgmt-table tr:first-of-type th {
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
table#postbox-static, div#report-delbox {
|
||||
table#postbox-static, div#report-delbox, form#filterform {
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
input[type="file"]::file-selector-button,
|
||||
input[type="file"]::-webkit-file-upload-button {
|
||||
border-radius: 5px;
|
||||
@extend %formstyle;
|
||||
}
|
||||
select {
|
||||
@extend %darkselect;
|
||||
}
|
||||
button, input[type=submit] {
|
||||
background:#16171A;
|
||||
background: colors.$inputbg2;
|
||||
}
|
||||
}
|
||||
|
||||
select#changepage {
|
||||
@extend %formstyle;
|
||||
button.hideblock-button,
|
||||
select#changepage,
|
||||
select.post-actions,
|
||||
select#boardsearch,
|
||||
select[name="show"] {
|
||||
@extend %darkselect;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
@import 'util';
|
||||
@import 'burichan/colors';
|
||||
@import 'burichan/img';
|
||||
@import 'burichan/front';
|
||||
@import 'burichan/manage';
|
||||
@use 'util';
|
||||
@use 'burichan/colors';
|
||||
@use 'burichan/img';
|
||||
@use 'burichan/front';
|
||||
@use 'burichan/manage';
|
||||
|
||||
body {
|
||||
font: $font;
|
||||
background: $bgcol;
|
||||
font: colors.$font;
|
||||
background: colors.$bgcol;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: $linkcol;
|
||||
color: colors.$linkcol;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
@ -32,13 +32,11 @@ h2 a {
|
|||
|
||||
h3 {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
h1,h2 {
|
||||
// background:#D6DAF0;
|
||||
font-family: $hfont-family;
|
||||
h1, h2 {
|
||||
font-family: colors.$hfont-family;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
|
@ -47,21 +45,32 @@ h1, h2, h3 {
|
|||
}
|
||||
|
||||
div#topbar {
|
||||
// @include box-shadow(3px 3px 5px 6px $shadowcol);
|
||||
@include shadow-filter(3px 5px 6px $shadowcol);
|
||||
height: 30px;
|
||||
@include util.shadow-filter(3px 5px 6px colors.$shadowcol);
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
div#topbar,
|
||||
.topbar-item,
|
||||
.topbar-item:visited,
|
||||
a.topbar-item,
|
||||
a.topbar-item:visited,
|
||||
.dropdown-button,
|
||||
.dropdown-menu/* ,
|
||||
.dropdown-menu a */{
|
||||
background: #000A89;
|
||||
color: $bgcol;
|
||||
div.dropdown-menu {
|
||||
background: #080e5e;
|
||||
color: colors.$bgcol;
|
||||
}
|
||||
|
||||
div#footer {
|
||||
font-size: 8pt;
|
||||
div.dropdown-menu {
|
||||
@include util.shadow-filter(3px 5px 6px colors.$shadowcol);
|
||||
a, h3 {
|
||||
color: colors.$bgcol;
|
||||
}
|
||||
}
|
||||
|
||||
a.topbar-item:hover,
|
||||
a.topbar-item:active,
|
||||
div.dropdown-menu a:hover {
|
||||
background: #0a127b;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: 0.75em;
|
||||
}
|
|
@ -1,26 +1,27 @@
|
|||
@import 'colors';
|
||||
@import '../global/colors';
|
||||
@use 'colors';
|
||||
@use '../global/colors' as global-colors;
|
||||
@use "../util";
|
||||
|
||||
h1#board-title {
|
||||
font-family: serif;
|
||||
font-size: 24pt;
|
||||
color: $headercol;
|
||||
font-size: 2em;
|
||||
color: global-colors.$headercol;
|
||||
}
|
||||
|
||||
.postblock {
|
||||
background:$postblock;
|
||||
background:colors.$postblock;
|
||||
}
|
||||
|
||||
div.file-info {
|
||||
font-size:12px;
|
||||
font-family:sans-serif;
|
||||
font-size: 1em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
span.postername {
|
||||
font-size:12px;
|
||||
font-family:serif;
|
||||
color: $namecol;
|
||||
font-weight:800;
|
||||
font-size: 1em;
|
||||
font-family: serif;
|
||||
color: global-colors.$namecol;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
div.reply,
|
||||
|
@ -31,7 +32,14 @@ div.inlinepostprev {
|
|||
}
|
||||
|
||||
select.post-actions,
|
||||
div.reply,
|
||||
div.postprev,
|
||||
div.inlinepostprev {
|
||||
border: 1px solid $bordercol;
|
||||
}
|
||||
border: 1px solid colors.$bordercol;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #222;
|
||||
}
|
||||
|
||||
@include util.upload-box(#aaa, #444, #666);
|
|
@ -1,17 +1,17 @@
|
|||
@import 'colors';
|
||||
@import '../util';
|
||||
@use 'colors';
|
||||
@use '../util';
|
||||
|
||||
.loginbox input {
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.manage-header {
|
||||
background: $bgcol;
|
||||
background: colors.$bgcol;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
table.mgmt-table {
|
||||
tr:first-of-type th {
|
||||
background: $postblock;
|
||||
background: colors.$postblock;
|
||||
}
|
||||
}
|
|
@ -1,51 +1,51 @@
|
|||
@import 'clear/colors';
|
||||
@import './util';
|
||||
@use 'clear/colors';
|
||||
@use 'util';
|
||||
|
||||
body {
|
||||
background: $bgcol;
|
||||
background: colors.$bgcol;
|
||||
font-family: monospace, sans-serif;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
background: $topbarbg;
|
||||
border-bottom: 1px solid $topbarborder;
|
||||
background: colors.$topbarbg;
|
||||
border-bottom: 1px solid colors.$topbarborder;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $linkcol;
|
||||
color: colors.$linkcol;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-shadow: 0px 0px 5px $hrcol;
|
||||
text-shadow: 0px 0px 5px colors.$hrcol;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
color: $headercol;
|
||||
color: colors.$headercol;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid $hrcol;
|
||||
border-top: 1px solid colors.$hrcol;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
div#content {
|
||||
input, select, textarea {
|
||||
border: 1px double $inputshadow;
|
||||
border: 1px double colors.$inputshadow;
|
||||
border-radius: 5px;
|
||||
background: $inputcol;
|
||||
background: colors.$inputcol;
|
||||
color: #000;
|
||||
}
|
||||
input:active, select:active, textarea:active {
|
||||
@include shadow-filter(0px 0px 5px $replyborder);
|
||||
@include util.shadow-filter(0px 0px 5px colors.$replyborder);
|
||||
}
|
||||
input[type="button"],
|
||||
input[type="submit"],
|
||||
input[type="file"]::file-selector-button,
|
||||
input[type="file"]::webkit-file-upload-button {
|
||||
background: #A7A7A7;
|
||||
border: 3px double $inputshadow;
|
||||
border: 3px double colors.$inputshadow;
|
||||
border-radius: 5px;
|
||||
color: #000;
|
||||
}
|
||||
|
@ -58,14 +58,14 @@ th.postblock,
|
|||
div.postprev,
|
||||
div.inlinepostprev,
|
||||
table.mgmt-table tr:first-of-type th {
|
||||
background: $inputcol;
|
||||
border: 1px solid $replyborder;
|
||||
background: colors.$inputcol;
|
||||
border: 1px solid colors.$replyborder;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.subject {
|
||||
font-weight: bold;
|
||||
color: $subjectcol;
|
||||
font-weight: 800;
|
||||
color: colors.$subjectcol;
|
||||
}
|
||||
|
||||
table#pages, table#pages * {
|
||||
|
@ -74,5 +74,7 @@ table#pages, table#pages * {
|
|||
|
||||
div.section-title-block {
|
||||
background: #A7A7A7;
|
||||
border-bottom: 1px solid $replyborder;
|
||||
}
|
||||
border-bottom: 1px solid colors.$replyborder;
|
||||
}
|
||||
|
||||
@include util.upload-box(#aaa, #444, #666);
|
|
@ -1,60 +1,55 @@
|
|||
@import 'dark/colors';
|
||||
@use 'dark/colors';
|
||||
|
||||
body {
|
||||
background: $bgcol;
|
||||
font-family: $font-family;
|
||||
color: $color;
|
||||
font-size: 80%;
|
||||
background: colors.$bgcol;
|
||||
font-family: colors.$font-family;
|
||||
color: colors.$color;
|
||||
// font-size: 80%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $linkcol;
|
||||
color: colors.$linkcol;
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
background: $topbarbg;
|
||||
border-bottom: 1px solid $color;
|
||||
background: colors.$topbarbg;
|
||||
border-bottom: 1px solid colors.$color;
|
||||
a {
|
||||
text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
color: $headercol;
|
||||
color: colors.$headercol;
|
||||
}
|
||||
|
||||
div#content {
|
||||
// input:not([type=submit]):not([type=button]),
|
||||
input:not(div#qrbuttons input),
|
||||
textarea, select {
|
||||
background: $inputbg;
|
||||
border: 1px solid $topbarbg;
|
||||
color: $linkcol;
|
||||
background: colors.$inputbg;
|
||||
border: 1px solid colors.$topbarbg;
|
||||
color: colors.$linkcol;
|
||||
}
|
||||
}
|
||||
|
||||
th.postblock, table.mgmt-table tr:first-of-type th {
|
||||
background: $blockcol;
|
||||
border: 1px solid $blockborder;
|
||||
background: colors.$blockcol;
|
||||
border: 1px solid colors.$blockborder;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
// select.post-actions {
|
||||
// color: $color;
|
||||
// }
|
||||
|
||||
div.reply,
|
||||
div.postprev,
|
||||
div.inlinepostprev {
|
||||
background: $inputbg;
|
||||
border: 1px solid $replyborder;
|
||||
background: colors.$inputbg;
|
||||
border: 1px solid colors.$replyborder;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
span.postername {
|
||||
color: $headercol;
|
||||
color: colors.$headercol;
|
||||
font-weight: bold;
|
||||
a {
|
||||
text-decoration: underline;
|
||||
|
@ -62,11 +57,18 @@ span.postername {
|
|||
}
|
||||
|
||||
span.tripcode {
|
||||
color: $headercol;
|
||||
color: colors.$headercol;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background: colors.$topbarbg!important;
|
||||
b {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
span.subject {
|
||||
color: $subjectcol;
|
||||
color: colors.$subjectcol;
|
||||
}
|
||||
|
||||
table#pages, table#pages * {
|
||||
|
|
|
@ -1,59 +1,59 @@
|
|||
@import './darkbunker/img';
|
||||
@import './darkbunker/vars';
|
||||
@import './util';
|
||||
@use 'darkbunker/img';
|
||||
@use 'darkbunker/vars';
|
||||
@use 'util';
|
||||
|
||||
body {
|
||||
background: $bgcol;
|
||||
color: $txtcol;
|
||||
background: vars.$bgcol;
|
||||
color: vars.$txtcol;
|
||||
font-family: arial,helvetica,sans-serif;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
background: $topbarcol;
|
||||
border-bottom: 2px solid $linkcol;
|
||||
background: vars.$topbarcol;
|
||||
border-bottom: 2px solid vars.$linkcol;
|
||||
* {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
color: $bordercol;
|
||||
color: vars.$bordercol;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $linkcol;
|
||||
color: vars.$linkcol;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: inherit;
|
||||
color: $linklight;
|
||||
color: vars.$linklight;
|
||||
}
|
||||
|
||||
header {
|
||||
h1, div#board-subtitle {
|
||||
font-family: tahoma;
|
||||
color: $headercol;
|
||||
color: vars.$headercol;
|
||||
}
|
||||
}
|
||||
|
||||
th, div.reply {
|
||||
background: $gridcol;
|
||||
border: 1px solid $bordercol;
|
||||
background: vars.$gridcol;
|
||||
border: 1px solid vars.$bordercol;
|
||||
}
|
||||
|
||||
div.section-block {
|
||||
border: 1px solid white;
|
||||
|
||||
div.section-title-block {
|
||||
background: $blocktitle;
|
||||
background: vars.$blocktitle;
|
||||
border: 1px solid white;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
div#frontpage div.section-block:first-child {
|
||||
background: $gridcol;
|
||||
background: vars.$gridcol;
|
||||
// border: none;
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
@import './vars';
|
||||
@use 'vars';
|
||||
|
||||
div#content, div#qr-box {
|
||||
color: $txtcol;
|
||||
border: 1px double $inputshadow;
|
||||
color: vars.$txtcol;
|
||||
border: 1px double vars.$inputshadow;
|
||||
border-radius: 5px;
|
||||
|
||||
textarea,
|
||||
input:not([type="file"]):not([type="checkbox"]),
|
||||
[type="submit"],
|
||||
select {
|
||||
color: $txtcol;
|
||||
background: $gridcol;
|
||||
border: 1px solid $inputshadow;
|
||||
color: vars.$txtcol;
|
||||
background: vars.$gridcol;
|
||||
border: 1px solid vars.$inputshadow;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
div#qr-box {
|
||||
background: $gridcol;
|
||||
background: vars.$gridcol;
|
||||
}
|
||||
|
||||
form#postform, form#qrpostform {
|
||||
|
@ -29,5 +29,5 @@ form#postform, form#qrpostform {
|
|||
|
||||
div#staffmenu, div#watchermenu {
|
||||
background: black;
|
||||
border: 1px solid $txtcol;
|
||||
border: 1px solid vars.$txtcol;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
@import 'global/img';
|
||||
@import 'global/front';
|
||||
@import 'global/manage';
|
||||
@import 'global/lightbox';
|
||||
@import 'global/qr';
|
||||
@import "global/watcher";
|
||||
@import 'global/bans';
|
||||
@use 'global/img';
|
||||
@use 'global/front';
|
||||
@use 'global/manage';
|
||||
@use 'global/lightbox';
|
||||
@use 'global/qr';
|
||||
@use "global/watcher";
|
||||
@use 'global/bans';
|
||||
@use 'global/animations';
|
||||
|
||||
.increase-line-height {
|
||||
header, .post, .reply {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
margin-top:50px;
|
||||
text-align:center;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
h1 {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
left:0;
|
||||
position:fixed;
|
||||
top:0;
|
||||
width:100%;
|
||||
margin-top:0px;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
margin-top: 0px;
|
||||
|
||||
* {
|
||||
padding: 4px;
|
||||
|
@ -63,15 +70,15 @@ div#staffmenu {
|
|||
div.section-block {
|
||||
margin-bottom: 8px;
|
||||
div.section-title-block {
|
||||
display:block;
|
||||
padding:4px 8px 4px 8px;
|
||||
display: block;
|
||||
padding: 4px 8px 4px 8px;
|
||||
}
|
||||
div.section-body {
|
||||
overflow-y: hidden;
|
||||
margin-right: 0px;
|
||||
margin-bottom:8px;
|
||||
min-height: 48px;
|
||||
padding:8px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,13 +87,13 @@ div.section-block {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#footer {
|
||||
bottom:0px;
|
||||
clear:both;
|
||||
left:0px;
|
||||
position:static;
|
||||
text-align:center;
|
||||
width:100%;
|
||||
footer {
|
||||
bottom: 0px;
|
||||
clear: both;
|
||||
left: 0px;
|
||||
position: static;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
select.post-actions {
|
||||
|
|
21
frontend/sass/global/_animations.scss
Normal file
21
frontend/sass/global/_animations.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@keyframes slideopen {
|
||||
from {
|
||||
transform: scale(1, 0);
|
||||
transform-origin: top center;
|
||||
}
|
||||
to {
|
||||
transform: scale(1, 1);
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideclose {
|
||||
from {
|
||||
transform: scale(1, 1);
|
||||
transform-origin: top center;
|
||||
}
|
||||
to {
|
||||
transform: scale(1, 0);
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
@use 'animations';
|
||||
|
||||
#boardmenu-bottom {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
@ -218,4 +220,30 @@ div.inlinepostprev {
|
|||
overflow: hidden;
|
||||
margin:8px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.hideblock {
|
||||
border: 1px solid black;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hideblock.open {
|
||||
animation: slideopen 0.25s;
|
||||
}
|
||||
|
||||
.hideblock.close {
|
||||
animation: slideclose 0.25s;
|
||||
}
|
||||
|
||||
.hideblock.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hideblock-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #aaa;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
@import '../util';
|
||||
@use '../util';
|
||||
|
||||
.lightbox {
|
||||
background:#CDCDCD;
|
||||
|
@ -12,12 +12,10 @@
|
|||
top:5%;
|
||||
z-index:9001;
|
||||
overflow: auto;
|
||||
// display:hidden;
|
||||
}
|
||||
|
||||
.lightbox * {
|
||||
// box-shadow: 0px 0px 0px 0px #000000;
|
||||
@include shadow-filter(0px 0px 0px #000);
|
||||
@include util.shadow-filter(0px 0px 0px #000);
|
||||
color:#000000;
|
||||
}
|
||||
|
||||
|
@ -45,19 +43,17 @@
|
|||
}
|
||||
|
||||
.lightbox-title {
|
||||
font-size:42px;
|
||||
font-weight:700;
|
||||
text-align:center;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lightbox-x {
|
||||
color: #000!important;
|
||||
float: right;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.lightbox-x:hover {
|
||||
.lightbox-x:hover,.lightbox-x:active {
|
||||
color:#555;
|
||||
}
|
||||
|
||||
|
@ -70,14 +66,14 @@
|
|||
.lightbox input[type=password],
|
||||
.lightbox input[type=file],
|
||||
.lightbox textarea {
|
||||
background:#FFF;
|
||||
border:1px solid #000;
|
||||
color:#000;
|
||||
background: #FFF;
|
||||
border: 1px solid #000;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.lightbox textarea#sql-statement {
|
||||
width:95%;
|
||||
height:300px;
|
||||
width: 95%;
|
||||
height: 300px;
|
||||
margin-left: 0px;
|
||||
clear: both;
|
||||
background: #FFF;
|
||||
|
@ -92,8 +88,7 @@
|
|||
border: 1px solid #000;
|
||||
border-radius: 0px;
|
||||
background: #777;
|
||||
// box-shadow: 0px 0px 0px 0px #000000;
|
||||
@include shadow-filter(0px 0px 0px #000);
|
||||
@include util.shadow-filter(0px 0px 0px #000);
|
||||
}
|
||||
|
||||
#settings-container table textarea {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
@import '../util';
|
||||
@use '../util';
|
||||
|
||||
div#qr-box {
|
||||
padding:1px;
|
||||
@include box-sizing(border-box);
|
||||
@include util.box-sizing(border-box);
|
||||
min-width: 300px;
|
||||
background:lightgray;
|
||||
border: 1px solid #000;
|
||||
|
||||
input[type=text],textarea {
|
||||
display: table-cell;
|
||||
min-width:300px;
|
||||
min-width:320px;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
width:100%;
|
||||
@include box-sizing(border-box);
|
||||
@include util.box-sizing(border-box);
|
||||
}
|
||||
input[type=file] {
|
||||
background: lightgray;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
@import 'photon/colors';
|
||||
@import 'photon/img';
|
||||
@import 'util';
|
||||
@use 'photon/colors';
|
||||
@use 'photon/img';
|
||||
@use 'util';
|
||||
|
||||
body {
|
||||
background: $bgcol;
|
||||
font: 15px $font;
|
||||
background: colors.$bgcol;
|
||||
font: 15px colors.$font;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -18,23 +18,25 @@ div#topbar, div.dropdown-menu {
|
|||
}
|
||||
|
||||
div#topbar {
|
||||
@include shadow-filter(0px 2px 2px $shadowcol);
|
||||
@include util.shadow-filter(0px 2px 2px colors.$shadowcol);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
color: #FFF;
|
||||
background: #000!important;
|
||||
@include shadow-filter(2px 2px 3px $shadowcol);
|
||||
@include util.shadow-filter(2px 2px 3px colors.$shadowcol);
|
||||
z-index: 0;
|
||||
div:hover {
|
||||
background: $dropdowncol;
|
||||
background: colors.$dropdowncol;
|
||||
}
|
||||
}
|
||||
|
||||
header, div#top-pane, a {
|
||||
color: $linkcol;
|
||||
color: colors.$linkcol;
|
||||
}
|
||||
|
||||
#site-title, #board-title {
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
@include util.upload-box(#aaa, #444, #666);
|
|
@ -1,25 +1,25 @@
|
|||
@import '../util';
|
||||
@import 'colors';
|
||||
@use '../util';
|
||||
@use 'colors';
|
||||
|
||||
div.reply,
|
||||
div.postprev,
|
||||
div.inlinepostprev,
|
||||
th.postblock {
|
||||
background: $replycol;
|
||||
border: 1px solid $replyborder;
|
||||
@include border-radius(5px);
|
||||
background: colors.$replycol;
|
||||
border: 1px solid colors.$replyborder;
|
||||
@include util.border-radius(5px);
|
||||
}
|
||||
|
||||
span.subject {
|
||||
color: $subjectcol;
|
||||
color: colors.$subjectcol;
|
||||
}
|
||||
|
||||
div.section-title-block {
|
||||
background: $replyborder;
|
||||
background: colors.$replyborder;
|
||||
}
|
||||
|
||||
div.section-block {
|
||||
@include border-radius(5px);
|
||||
background: $sectionbg;
|
||||
border: 1px solid $sectionborder;
|
||||
@include util.border-radius(5px);
|
||||
background: colors.$sectionbg;
|
||||
border: 1px solid colors.$sectionborder;
|
||||
}
|
|
@ -1,73 +1,73 @@
|
|||
@import 'pipes/colors';
|
||||
@import 'pipes/front';
|
||||
@import 'pipes/manage';
|
||||
@import 'pipes/img';
|
||||
@import 'util';
|
||||
@use 'pipes/colors';
|
||||
@use 'pipes/front';
|
||||
@use 'pipes/manage';
|
||||
@use 'pipes/img';
|
||||
@use 'util';
|
||||
|
||||
* {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body {
|
||||
background: $bgcol;
|
||||
background: colors.$bgcol;
|
||||
background-image: url(res/pipes_bg.png);
|
||||
color: #d8d0b9;
|
||||
font: $font;
|
||||
font: colors.$font;
|
||||
}
|
||||
|
||||
header h1, #site-title {
|
||||
color: $headercol;
|
||||
color: colors.$headercol;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $linkcol;
|
||||
font: $font;
|
||||
color: colors.$linkcol;
|
||||
font: colors.$font;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: inherit;
|
||||
color: $linklight;
|
||||
color: colors.$linklight;
|
||||
}
|
||||
|
||||
a.topbar-item:hover {
|
||||
background: $bglight;
|
||||
background: colors.$bglight;
|
||||
}
|
||||
|
||||
div#footer, div#footer * {
|
||||
footer, footer * {
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
div#topbar {
|
||||
background: $topbarcol;
|
||||
background: colors.$topbarcol;
|
||||
// @include box-shadow(0px 2px 2px 3px $shadowcol);
|
||||
@include shadow-filter(0px 4px 2px $shadowcol);
|
||||
@include util.shadow-filter(0px 4px 2px colors.$shadowcol);
|
||||
|
||||
li:hover {
|
||||
background: $bglight;
|
||||
background: colors.$bglight;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background: $topbarcol!important;
|
||||
@include shadow-filter(2px 2px 3px $shadowcol);
|
||||
background: colors.$topbarcol!important;
|
||||
@include util.shadow-filter(2px 2px 3px colors.$shadowcol);
|
||||
z-index: 0;
|
||||
div:hover {
|
||||
background: $bglight;
|
||||
background: colors.$bglight;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-tabs-tab {
|
||||
background: $topbarcol;
|
||||
border: 1px solid $bglight;
|
||||
background: colors.$topbarcol;
|
||||
border: 1px solid colors.$bglight;
|
||||
}
|
||||
|
||||
.ui-tabs-active {
|
||||
background: $bgcol;
|
||||
background: colors.$bgcol;
|
||||
}
|
||||
|
||||
.ui-tabs-panel {
|
||||
background: $bgcol;
|
||||
border: 1px solid $topbarcol;
|
||||
background: colors.$bgcol;
|
||||
border: 1px solid colors.$topbarcol;
|
||||
padding: 8px;
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
@import 'colors';
|
||||
@import '../util';
|
||||
@use 'colors';
|
||||
@use '../util';
|
||||
|
||||
.dropdown-button:hover {
|
||||
background: $bglight;
|
||||
background: colors.$bglight;
|
||||
}
|
||||
|
||||
.section-body {
|
||||
|
@ -10,27 +10,27 @@
|
|||
}
|
||||
|
||||
.section-title-block {
|
||||
background: $topbarcol;
|
||||
background: colors.$topbarcol;
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
background: $bglight;
|
||||
border: 1px solid $bgcol;
|
||||
background: colors.$bglight;
|
||||
border: 1px solid colors.$bgcol;
|
||||
}
|
||||
|
||||
#current-tab {
|
||||
background: $bgcol;
|
||||
background: colors.$bgcol;
|
||||
}
|
||||
|
||||
div#recent-posts-header {
|
||||
// @include box-shadow(0px 2px 2px 3px $shadowcol);
|
||||
@include shadow-filter(0px 2px 3px $shadowcol);
|
||||
@include util.shadow-filter(0px 2px 3px colors.$shadowcol);
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 8px 4px 8px;
|
||||
}
|
||||
|
||||
.postblock {
|
||||
background: $bgcol;
|
||||
background: colors.$bgcol;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@import '_colors';
|
||||
@import '../util';
|
||||
@use '_colors';
|
||||
@use '../util';
|
||||
|
||||
.dropdown-button:hover {
|
||||
background: $bglight;
|
||||
background: colors.$bglight;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
|
@ -12,13 +12,13 @@ img.thumbnail {
|
|||
}
|
||||
|
||||
.reply, .inlinepostprev, .postprev {
|
||||
background: $postblock;
|
||||
border: 1px solid $postblockcol;
|
||||
background: colors.$postblock;
|
||||
border: 1px solid colors.$postblockcol;
|
||||
}
|
||||
|
||||
.postblock,
|
||||
table.mgmt-table tr:first-of-type th {
|
||||
background: $postblock;
|
||||
background: colors.$postblock;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
@ -45,8 +45,8 @@ div#content {
|
|||
textarea,
|
||||
select#changepage,
|
||||
select.post-actions {
|
||||
background: $postblock;
|
||||
border: 1px solid $postblockcol;
|
||||
background: colors.$postblock;
|
||||
border: 1px solid colors.$postblockcol;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
@import 'colors';
|
||||
@import '../util';
|
||||
@use 'colors';
|
||||
@use '../util';
|
||||
|
||||
.loginbox input {
|
||||
height: 20%;
|
||||
}
|
||||
|
||||
.manage-header {
|
||||
background: $topbarcol;
|
||||
background: colors.$topbarcol;
|
||||
// @include box-shadow(2px 2px 3px 4px $shadowcol);
|
||||
@include shadow-filter(4px 4px 4px $shadowcol);
|
||||
@include util.shadow-filter(4px 4px 4px colors.$shadowcol);
|
||||
border-radius: 8px;
|
||||
}
|
|
@ -1,27 +1,27 @@
|
|||
@import 'win9x/vars';
|
||||
@use 'win9x/vars';
|
||||
|
||||
@font-face {
|
||||
@include mssans-font(400, normal);
|
||||
@include vars.mssans-font(400, normal);
|
||||
|
||||
}
|
||||
@font-face {
|
||||
@include mssans-font(700, normal);
|
||||
@include vars.mssans-font(700, normal);
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: $fontsize;
|
||||
font-size: vars.$fontsize;
|
||||
color: white;
|
||||
background: $bgcol;
|
||||
background: vars.$bgcol;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $txtcol;
|
||||
color: vars.$txtcol;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input {
|
||||
color: $txtcol;
|
||||
font-size: $fontsize!important;
|
||||
color: vars.$txtcol;
|
||||
font-size: vars.$fontsize!important;
|
||||
font-family: "Pixelated MS Sans Serif", Arial;
|
||||
outline: none;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ input {
|
|||
div#topbar {
|
||||
height: 26px;
|
||||
background: silver!important;
|
||||
box-shadow: $bar-shadow;
|
||||
box-shadow: vars.$bar-shadow;
|
||||
box-sizing: border-box;
|
||||
border: none!important;
|
||||
border-radius: 0!important;
|
||||
|
@ -39,9 +39,9 @@ div#topbar {
|
|||
}
|
||||
a[href="/"] {
|
||||
font-family: "Pixelated MS Sans Serif", Arial;
|
||||
font-size: $fontsize;
|
||||
font-size: vars.$fontsize;
|
||||
content: "Start"!important;
|
||||
background: $start_path!important;
|
||||
background: vars.$start_path!important;
|
||||
background-repeat: no-repeat!important;
|
||||
background-position-x: 3px!important;
|
||||
background-position-y: 4px!important;
|
||||
|
@ -54,12 +54,12 @@ div#topbar {
|
|||
div#topbar,
|
||||
div#topbar a {
|
||||
background: silver!important;
|
||||
box-shadow: $bar-shadow;
|
||||
box-shadow: vars.$bar-shadow;
|
||||
box-sizing: border-box;
|
||||
border: none!important;
|
||||
border-radius: 0!important;
|
||||
height:24px;
|
||||
color: $txtcol!important;
|
||||
color: vars.$txtcol!important;
|
||||
font-family: "Pixelated MS Sans Serif", Arial;
|
||||
font-size: 11px;
|
||||
cursor: default;
|
||||
|
@ -70,10 +70,10 @@ div#content {
|
|||
input[type=button],
|
||||
input[role=pushbutton],
|
||||
input[type=submit] {
|
||||
color: $txtcol!important;
|
||||
color: vars.$txtcol!important;
|
||||
/* font-size: 12px!important; */
|
||||
background: silver!important;
|
||||
box-shadow: $bar-shadow;
|
||||
box-shadow: vars.$bar-shadow;
|
||||
box-sizing: border-box;
|
||||
border: none!important;
|
||||
border-radius: 0!important;
|
||||
|
@ -95,7 +95,7 @@ div#qr-title {
|
|||
background: linear-gradient(90deg,navy,#1084d0);
|
||||
a {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
box-shadow: $bar-shadow;
|
||||
box-shadow: vars.$bar-shadow;
|
||||
min-width: 16px;
|
||||
min-height: 14px;
|
||||
display: block;
|
||||
|
@ -105,7 +105,7 @@ div#qr-title {
|
|||
background-position: top 3px left 40px!important;
|
||||
}
|
||||
a:hover {
|
||||
color:$txtcol;
|
||||
color:vars.$txtcol;
|
||||
}
|
||||
span#qr-buttons * {
|
||||
display: inline-block;
|
||||
|
@ -125,7 +125,7 @@ div#qr-box {
|
|||
box-shadow: inset -1px -1px #fff,inset 1px 1px grey,inset -2px -2px #dfdfdf,inset 2px 2px #0a0a0a;
|
||||
margin: 0;
|
||||
font-family: "Pixelated MS Sans Serif",Arial;
|
||||
color: $txtcol;
|
||||
color: vars.$txtcol;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ input[type=text] {
|
|||
} */
|
||||
|
||||
a.boardlistactive {
|
||||
color: $txtcol!important;
|
||||
color: vars.$txtcol!important;
|
||||
padding: 6px 3px!important;
|
||||
outline: 1px dotted #000;
|
||||
outline-offset: -4px;
|
||||
|
@ -148,7 +148,7 @@ a.boardlistactive {
|
|||
|
||||
|
||||
h1, h2, div.subtitle, a#qrDisplayButton {
|
||||
color: $txtcol;
|
||||
color: vars.$txtcol;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ div#content select {
|
|||
}
|
||||
|
||||
div.pages {
|
||||
background: $bgcol;
|
||||
background: vars.$bgcol;
|
||||
border: none;
|
||||
color: white;
|
||||
/* border-right: black;
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
@import 'global/colors';
|
||||
@import 'yotsuba/colors';
|
||||
@import 'yotsubacommon';
|
||||
@use 'global/colors' as global-colors;
|
||||
@use 'yotsuba/colors' as yotsuba-colors;
|
||||
@use 'yotsubacommon';
|
||||
|
||||
@include yotsuba(
|
||||
@include yotsubacommon.yotsuba(
|
||||
'res/yotsuba_bg.png',
|
||||
$bgcol, /* $bodybg */
|
||||
yotsuba-colors.$bgcol, /* $bodybg */
|
||||
maroon, /* $bodycol */
|
||||
$topbarbg,
|
||||
$topbarborder,
|
||||
$headercol,
|
||||
$postblockbg,
|
||||
$postblockborder,
|
||||
$postblockborder, /* $sectiontitlecol */
|
||||
yotsuba-colors.$topbarbg,
|
||||
yotsuba-colors.$topbarborder,
|
||||
global-colors.$headercol,
|
||||
yotsuba-colors.$postblockbg,
|
||||
yotsuba-colors.$postblockborder,
|
||||
yotsuba-colors.$postblockborder, /* $sectiontitlecol */
|
||||
#fca, /* $sectiontitlebg */
|
||||
$borderbotright,
|
||||
yotsuba-colors.$borderbotright,
|
||||
maroon, /* $linkcol */
|
||||
#D9BfB7, /* $hrcol */
|
||||
#CC1105, /* $subjectcol */
|
||||
$namecol,
|
||||
$replybg,
|
||||
global-colors.$namecol,
|
||||
yotsuba-colors.$replybg,
|
||||
navy /* $postlinkcol */
|
||||
);
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
@import 'global/colors';
|
||||
@import 'yotsubab/colors';
|
||||
@import 'yotsubacommon';
|
||||
@use 'global/colors' as global-colors;
|
||||
@use 'yotsubab/colors' as yotsubab-colors;
|
||||
@use 'yotsubacommon';
|
||||
|
||||
@include yotsuba(
|
||||
@include yotsubacommon.yotsuba(
|
||||
'res/yotsubab_bg.png',
|
||||
$bgcol, /* $bodybg */
|
||||
yotsubab-colors.$bgcol, /* $bodybg */
|
||||
#000, /* $bodycol */
|
||||
#D6DAF0, /* $topbarbg */
|
||||
$topbarborder,
|
||||
$headercol,
|
||||
$postblockbg,
|
||||
$postblockborder,
|
||||
yotsubab-colors.$topbarborder,
|
||||
global-colors.$headercol,
|
||||
yotsubab-colors.$postblockbg,
|
||||
yotsubab-colors.$postblockborder,
|
||||
#000, /* $sectiontitlecol */
|
||||
$postblockbg, /* $sectiontitlebg */
|
||||
$borderbotright,
|
||||
yotsubab-colors.$postblockbg, /* $sectiontitlebg */
|
||||
yotsubab-colors.$borderbotright,
|
||||
#34345C, /* $linkcol */
|
||||
#B7C5D9, /* $hrcol */
|
||||
#0F0C5D, /* $subjectcol */
|
||||
$namecol,
|
||||
$replybg,
|
||||
global-colors.$namecol,
|
||||
yotsubab-colors.$replybg,
|
||||
navy /* $postlinkcol */
|
||||
);
|
||||
|
|
|
@ -9,43 +9,42 @@ import { applyBBCode, handleKeydown } from "../ts/boardevents";
|
|||
|
||||
document.documentElement.innerHTML = simpleHTML;
|
||||
|
||||
function doBBCode(keycode: number, text: string, start: number, end: number) {
|
||||
function doBBCode(key:string, text: string, start: number, end: number) {
|
||||
const $ta = $<HTMLTextAreaElement>("<textarea/>");
|
||||
$ta.text(text);
|
||||
const e = $.Event("keydown");
|
||||
e.ctrlKey = true;
|
||||
$ta[0].selectionStart = start;
|
||||
$ta[0].selectionEnd = end;
|
||||
e.keyCode = keycode;
|
||||
e.which = keycode;
|
||||
e.key = key;
|
||||
$ta.first().trigger(e);
|
||||
applyBBCode(e as JQuery.KeyDownEvent);
|
||||
return $ta.text();
|
||||
}
|
||||
|
||||
test("Tests BBCode events", () => {
|
||||
let text = doBBCode(66, "bold", 0, 4);
|
||||
let text = doBBCode("b", "bold", 0, 4);
|
||||
expect(text).toEqual("[b]bold[/b]");
|
||||
text += "italics";
|
||||
text = doBBCode(73, text, text.length - 7, text.length);
|
||||
text = doBBCode("i", text, text.length - 7, text.length);
|
||||
expect(text).toEqual("[b]bold[/b][i]italics[/i]");
|
||||
|
||||
text = doBBCode(82, "strike" + text, 0, 6);
|
||||
text = doBBCode("r", "strike" + text, 0, 6);
|
||||
expect(text).toEqual("[s]strike[/s][b]bold[/b][i]italics[/i]");
|
||||
|
||||
text = doBBCode(83, text, 0, 13);
|
||||
text = doBBCode("s", text, 0, 13);
|
||||
expect(text).toEqual("[?][s]strike[/s][/?][b]bold[/b][i]italics[/i]");
|
||||
|
||||
text = doBBCode(85, text, text.length, text.length);
|
||||
text = doBBCode("u", text, text.length, text.length);
|
||||
expect(text).toEqual("[?][s]strike[/s][/?][b]bold[/b][i]italics[/i][u][/u]");
|
||||
|
||||
const invalidKeyCode = doBBCode(0, text, 0, 1); // passes an invalid keycode to applyBBCode, no change
|
||||
const invalidKeyCode = doBBCode("x", text, 0, 1); // passes an invalid keycode to applyBBCode, no change
|
||||
expect(invalidKeyCode).toEqual(text);
|
||||
});
|
||||
|
||||
test("Tests proper form submission via JS", () => {
|
||||
const $form = $("form#postform");
|
||||
const text = doBBCode(83, "text", 0, 4);
|
||||
const text = doBBCode("s", "text", 0, 4);
|
||||
$form.find("textarea#postmsg").text(text);
|
||||
let submitted = false;
|
||||
$form.on("submit", () => {
|
||||
|
@ -54,7 +53,7 @@ test("Tests proper form submission via JS", () => {
|
|||
});
|
||||
const e = $.Event("keydown");
|
||||
e.ctrlKey = true;
|
||||
e.keyCode = 10;
|
||||
e.key = "Enter";
|
||||
$form.find("textarea#postmsg").first().trigger(e);
|
||||
handleKeydown(e as JQuery.KeyDownEvent);
|
||||
expect(submitted).toBeTruthy();
|
||||
|
|
|
@ -9,7 +9,7 @@ export function removeLightbox(...customs: any) {
|
|||
|
||||
export function showLightBox(title: string, innerHTML: string) {
|
||||
$(document.body).prepend(
|
||||
`<div class="lightbox-bg"></div><div class="lightbox"><div class="lightbox-title">${title}<a href="javascript:;" class="lightbox-x">X</a><hr /></div>${innerHTML}</div>`
|
||||
`<div class="lightbox-bg"></div><div class="lightbox"><h1 class="lightbox-title">${title}<a href="javascript:;" class="lightbox-x">X</a><hr /></h1>${innerHTML}</div>`
|
||||
);
|
||||
$("a.lightbox-x, .lightbox-bg").on("click", removeLightbox);
|
||||
}
|
||||
|
|
|
@ -97,10 +97,10 @@ function deletePost(id: number, board: string, fileOnly = false) {
|
|||
alertLightbox(`Delete failed: ${data.error}`, "Error");
|
||||
}).done(data => {
|
||||
if(data.error === undefined || data === "") {
|
||||
if(location.href.indexOf(`/${board}/res/${id}.html`) > -1) {
|
||||
alertLightbox("Thread deleted", "Success");
|
||||
} else if(fileOnly) {
|
||||
if(fileOnly) {
|
||||
deletePostFile(id);
|
||||
} else if(location.href.indexOf(`/${board}/res/${id}.html`) > -1) {
|
||||
alertLightbox("Thread deleted", "Success");
|
||||
} else {
|
||||
deletePostElement(id);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import $ from "jquery";
|
|||
import { extname } from "path";
|
||||
import { formatDateString, formatFileSize } from "../formatting";
|
||||
import { getThumbFilename } from "../postinfo";
|
||||
import { getBooleanStorageVal } from "../storage";
|
||||
|
||||
/**
|
||||
* creates an element from the given post data
|
||||
|
@ -117,7 +118,35 @@ export function shrinkOriginalFilenames(elem = $(document.body)) {
|
|||
});
|
||||
}
|
||||
|
||||
export function prepareHideBlocks() {
|
||||
$("div.hideblock").each((_i,el) => {
|
||||
const $el = $(el);
|
||||
const $button = $("<button />").prop({
|
||||
class: "hideblock-button",
|
||||
}).text($el.hasClass("open") ? "Hide" : "Show").on("click", e => {
|
||||
e.preventDefault();
|
||||
const hidden = $el.hasClass("hidden");
|
||||
$button.text(hidden ? "Hide" : "Show");
|
||||
if(el.onanimationend === undefined || !getBooleanStorageVal("smoothhidetoggle", true)) {
|
||||
$el.toggleClass("hidden");
|
||||
} else {
|
||||
$el.removeClass("close");
|
||||
if(hidden) {
|
||||
$el.removeClass("hidden").addClass("open");
|
||||
} else {
|
||||
$el.addClass("close").removeClass("open");
|
||||
}
|
||||
}
|
||||
}).insertBefore($el);
|
||||
$el.on("animationend", () => {
|
||||
if($el.hasClass("close")) {
|
||||
$el.addClass("hidden").removeClass("close");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(() => {
|
||||
prepareHideBlocks();
|
||||
shrinkOriginalFilenames();
|
||||
});
|
|
@ -1,11 +1,9 @@
|
|||
import $ from "jquery";
|
||||
import "jquery-ui/ui/version";
|
||||
import "jquery-ui/ui/plugin";
|
||||
import "jquery-ui/ui/safe-active-element";
|
||||
import "jquery-ui/ui/widget";
|
||||
import "jquery-ui/ui/scroll-parent";
|
||||
import "jquery-ui/ui/widgets/mouse";
|
||||
import "jquery-ui/ui/safe-blur";
|
||||
import "jquery-ui/ui/widgets/draggable";
|
||||
|
||||
import { upArrow, downArrow } from "../vars";
|
||||
|
@ -93,6 +91,22 @@ function setButtonTimeout(prefix = "", cooldown = 5) {
|
|||
timeoutCB();
|
||||
}
|
||||
|
||||
function fixFileList() {
|
||||
let files:FileList = null;
|
||||
const $browseBtns = $<HTMLInputElement>("input[name=imagefile]");
|
||||
$browseBtns.each((_i, el) => {
|
||||
if(el.files?.length > 0 && !files) {
|
||||
files = el.files;
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
$browseBtns.each((_i, el) => {
|
||||
if(files)
|
||||
el.files = files;
|
||||
});
|
||||
}
|
||||
|
||||
export function initQR() {
|
||||
if($qr !== null) {
|
||||
// QR box already initialized
|
||||
|
@ -204,6 +218,7 @@ export function initQR() {
|
|||
resetSubmitButtonText();
|
||||
|
||||
$postform.on("submit", function(e) {
|
||||
fixFileList();
|
||||
const $form = $<HTMLFormElement>(this);
|
||||
e.preventDefault();
|
||||
copyCaptchaResponse($form);
|
||||
|
|
|
@ -54,7 +54,7 @@ function onReaderLoad(name:string, e:ProgressEvent<FileReader>) {
|
|||
"href": "#"
|
||||
}).text("X").on("click", (e:JQuery.ClickEvent) => {
|
||||
const $target = $(e.target);
|
||||
const $browseBtn = $target.parents<HTMLInputElement>("#upload-box").siblings("input[name=imagefile]");
|
||||
const $browseBtn = $target.parents<HTMLInputElement>("#upload-box").siblings<HTMLInputElement>("input[name=imagefile]");
|
||||
$browseBtn.each((_, el) => {
|
||||
el.value = null;
|
||||
});
|
||||
|
@ -86,13 +86,19 @@ function replaceBrowseButton() {
|
|||
$("<a/>").addClass("browse-text")
|
||||
.attr("href", "#")
|
||||
.text("Select/drop/paste upload here")
|
||||
.on("click", () => $browseBtn.trigger("click"))
|
||||
.on("click", e => {
|
||||
e.preventDefault();
|
||||
$browseBtn.trigger("click");
|
||||
})
|
||||
).on("dragenter dragover drop", dragAndDrop).insertBefore($browseBtn);
|
||||
|
||||
$("form#postform, form#qrpostform").on("paste", e => {
|
||||
const clipboardData = (e.originalEvent as ClipboardEvent).clipboardData;
|
||||
if(clipboardData.items.length < 1 || clipboardData.items[0].kind !== "file") {
|
||||
console.log("No files in clipboard");
|
||||
if(clipboardData.items.length < 1) {
|
||||
alertLightbox("No files in clipboard", "Unable to paste");
|
||||
return;
|
||||
}
|
||||
if(clipboardData.items[0].kind !== "file") {
|
||||
return;
|
||||
}
|
||||
const clipboardFile = clipboardData.items[0].getAsFile();
|
||||
|
|
|
@ -4,7 +4,7 @@ import "./vars";
|
|||
import "./cookies";
|
||||
import "./notifications";
|
||||
import { setPageBanner } from "./dom/banners";
|
||||
import { setCustomCSS, setCustomJS, setTheme } from "./settings";
|
||||
import { setCustomCSS, setCustomJS, setTheme, updateExternalLinks } from "./settings";
|
||||
import { handleKeydown } from "./boardevents";
|
||||
import { initStaff, createStaffMenu, addStaffThreadOptions } from "./management/manage";
|
||||
import { getPageThread } from "./postinfo";
|
||||
|
@ -57,6 +57,7 @@ $(() => {
|
|||
});
|
||||
$(document).on("keydown", handleKeydown);
|
||||
initFlags();
|
||||
updateExternalLinks();
|
||||
setCustomCSS();
|
||||
setCustomJS();
|
||||
});
|
||||
|
|
|
@ -8,11 +8,10 @@ function canNotify() {
|
|||
&& (typeof Notification !== "undefined");
|
||||
}
|
||||
|
||||
export function notify(title: string, body: string, img = noteIcon) {
|
||||
export function notify(title: string, body: string, icon = noteIcon) {
|
||||
const n = new Notification(title, {
|
||||
body: body,
|
||||
image: img,
|
||||
icon: noteIcon
|
||||
icon: icon
|
||||
});
|
||||
setTimeout(() => {
|
||||
n.close();
|
||||
|
|
|
@ -107,6 +107,7 @@ interface MinMax {
|
|||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
class NumberSetting extends Setting<number, HTMLInputElement> {
|
||||
constructor(key: string, title: string, defaultVal = 0, minMax: MinMax = {min: null, max: null}, onSave?:()=>any) {
|
||||
super(key, title, defaultVal, onSave);
|
||||
|
@ -205,6 +206,28 @@ export function setTheme() {
|
|||
else
|
||||
themeElem.setAttribute("href", path.join(webroot ?? "/", "css", style));
|
||||
}
|
||||
setLineHeight();
|
||||
}
|
||||
|
||||
function setLineHeight() {
|
||||
if(getBooleanStorageVal("increaselineheight", false)) {
|
||||
document.body.classList.add("increase-line-height");
|
||||
} else {
|
||||
document.body.classList.remove("increase-line-height");
|
||||
}
|
||||
}
|
||||
|
||||
export function updateExternalLinks(post?: JQuery<HTMLElement>) {
|
||||
const $src = post ?? $(".post-text");
|
||||
const extPostLinks = $src.find<HTMLAnchorElement>("a:not(.postref)");
|
||||
const newTab = getBooleanStorageVal("extlinksnewtab", true);
|
||||
for(const link of extPostLinks) {
|
||||
if(link.hostname !== location.hostname) {
|
||||
link.target = newTab?"_blank":"_self";
|
||||
} else {
|
||||
link.target = "_self";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -249,21 +272,23 @@ $(() => {
|
|||
}
|
||||
}) as Setting);
|
||||
settings.set("pintopbar", new BooleanSetting("pintopbar", "Pin top bar", true, initTopBar));
|
||||
settings.set("increaselineheight", new BooleanSetting("increaselineheight", "Increase line height", false, setLineHeight));
|
||||
settings.set("enableposthover", new BooleanSetting("enableposthover", "Preview post on hover", true, initPostPreviews));
|
||||
settings.set("enablepostclick", new BooleanSetting("enablepostclick", "Preview post on click", true, initPostPreviews));
|
||||
settings.set("useqr", new BooleanSetting("useqr", "Use Quick Reply box", true, () => {
|
||||
if(getBooleanStorageVal("useqr", true)) initQR();
|
||||
else closeQR();
|
||||
}));
|
||||
settings.set("extlinksnewtab", new BooleanSetting("extlinksnewtab", "Open external links in new tab", true, updateExternalLinks));
|
||||
settings.set("persistentqr", new BooleanSetting("persistentqr", "Persistent Quick Reply", false));
|
||||
settings.set("watcherseconds", new NumberSetting("watcherseconds", "Check watched threads every # seconds", 15, {
|
||||
min: 2
|
||||
}, initWatcher));
|
||||
settings.set("persistentqr", new BooleanSetting("persistentqr", "Persistent Quick Reply", false));
|
||||
settings.set("newuploader", new BooleanSetting("newuploader", "Use new upload element", true, updateBrowseButton));
|
||||
settings.set("smoothhidetoggle", new BooleanSetting("smoothhidetoggle", "Smooth hide block toggle", true));
|
||||
|
||||
settings.set("customjs", new TextSetting("customjs", "Custom JavaScript", ""));
|
||||
settings.set("customcss", new TextSetting("customcss", "Custom CSS", "", setCustomCSS));
|
||||
|
||||
if($settingsButton === null)
|
||||
$settingsButton = new TopBarButton("Settings", createLightbox, {before: "a#watcher"});
|
||||
$settingsButton ??= new TopBarButton("Settings", createLightbox, {before: "a#watcher"});
|
||||
});
|
2
go.mod
2
go.mod
|
@ -24,7 +24,7 @@ require (
|
|||
github.com/yuin/gopher-lua v1.1.1
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/net v0.32.0
|
||||
golang.org/x/net v0.33.0
|
||||
layeh.com/gopher-json v0.0.0-20201124131017-552bb3c4c3bf
|
||||
layeh.com/gopher-luar v1.0.11
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -189,8 +189,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
|
@ -17,20 +17,35 @@ div.section-body {
|
|||
background: #282A2E;
|
||||
}
|
||||
|
||||
select#changepage, table#postbox-static input,
|
||||
table#postbox-static select,
|
||||
table#postbox-static input,
|
||||
table#postbox-static textarea,
|
||||
table#postbox-static input[type=file]::file-selector-button,
|
||||
table#postbox-static input[type=file]::-webkit-file-upload-button, div#report-delbox input,
|
||||
div#report-delbox select,
|
||||
div#report-delbox textarea,
|
||||
div#report-delbox input[type=file]::file-selector-button,
|
||||
div#report-delbox input[type=file]::-webkit-file-upload-button {
|
||||
div#report-delbox input[type=file]::-webkit-file-upload-button, form#filterform input,
|
||||
form#filterform textarea,
|
||||
form#filterform input[type=file]::file-selector-button,
|
||||
form#filterform input[type=file]::-webkit-file-upload-button, button.hideblock-button,
|
||||
select#changepage,
|
||||
select.post-actions,
|
||||
select#boardsearch,
|
||||
select[name=show], table#postbox-static select, div#report-delbox select, form#filterform select {
|
||||
background: #282A2E;
|
||||
border: 1px double #07371F;
|
||||
color: #ACACAC;
|
||||
}
|
||||
|
||||
button.hideblock-button,
|
||||
select#changepage,
|
||||
select.post-actions,
|
||||
select#boardsearch,
|
||||
select[name=show], table#postbox-static select, div#report-delbox select, form#filterform select {
|
||||
background: #16171A;
|
||||
border-radius: 5px;
|
||||
color: #ACACAC;
|
||||
}
|
||||
|
||||
div.reply,
|
||||
th.postblock,
|
||||
div.postprev,
|
||||
|
@ -41,18 +56,7 @@ table.mgmt-table tr:first-of-type th {
|
|||
border-radius: 5px;
|
||||
}
|
||||
|
||||
table#postbox-static input,
|
||||
table#postbox-static select,
|
||||
table#postbox-static textarea,
|
||||
table#postbox-static input[type=file]::file-selector-button,
|
||||
table#postbox-static input[type=file]::-webkit-file-upload-button, div#report-delbox input,
|
||||
div#report-delbox select,
|
||||
div#report-delbox textarea,
|
||||
div#report-delbox input[type=file]::file-selector-button,
|
||||
div#report-delbox input[type=file]::-webkit-file-upload-button {
|
||||
border-radius: 5px;
|
||||
}
|
||||
table#postbox-static button, table#postbox-static input[type=submit], div#report-delbox button, div#report-delbox input[type=submit] {
|
||||
table#postbox-static button, table#postbox-static input[type=submit], div#report-delbox button, div#report-delbox input[type=submit], form#filterform button, form#filterform input[type=submit] {
|
||||
background: #16171A;
|
||||
}
|
||||
|
||||
|
@ -83,11 +87,21 @@ div#staff, select.post-actions {
|
|||
|
||||
div#topbar,
|
||||
div#topbar a,
|
||||
div#topbar a:visited {
|
||||
div#topbar a:visited,
|
||||
div.dropdown-menu {
|
||||
background: #151515;
|
||||
border-bottom: 1px solid #B0790A;
|
||||
}
|
||||
|
||||
div.dropdown-menu {
|
||||
border-left: 1px solid #B0790A;
|
||||
border-right: 1px solid #B0790A;
|
||||
}
|
||||
|
||||
div.dropdown-menu a:hover, div.dropdown-menu a:active {
|
||||
background: #1D1F21;
|
||||
}
|
||||
|
||||
table#pages, table#pages * {
|
||||
border: none;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
h1#board-title {
|
||||
font-family: serif;
|
||||
font-size: 24pt;
|
||||
font-size: 2em;
|
||||
color: #AF0A0F;
|
||||
}
|
||||
|
||||
|
@ -9,12 +9,12 @@ h1#board-title {
|
|||
}
|
||||
|
||||
div.file-info {
|
||||
font-size: 12px;
|
||||
font-size: 1em;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
span.postername {
|
||||
font-size: 12px;
|
||||
font-size: 1em;
|
||||
font-family: serif;
|
||||
color: #117743;
|
||||
font-weight: 800;
|
||||
|
@ -28,11 +28,26 @@ div.inlinepostprev {
|
|||
}
|
||||
|
||||
select.post-actions,
|
||||
div.reply,
|
||||
div.postprev,
|
||||
div.inlinepostprev {
|
||||
border: 1px solid #9295a4;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #222;
|
||||
}
|
||||
|
||||
div#upload-box {
|
||||
background: #aaa;
|
||||
}
|
||||
div#upload-box a {
|
||||
color: #444;
|
||||
}
|
||||
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#site-title {
|
||||
font-family: sans-serif;
|
||||
padding-top: 88px;
|
||||
|
@ -98,7 +113,6 @@ h2 a {
|
|||
|
||||
h3 {
|
||||
margin: 0px;
|
||||
text-align: center;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
|
@ -114,18 +128,32 @@ h1, h2, h3 {
|
|||
div#topbar {
|
||||
-webkit-filter: drop-shadow(3px 5px 6px #555555);
|
||||
filter: drop-shadow(3px 5px 6px #555555);
|
||||
height: 30px;
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
div#topbar,
|
||||
.topbar-item,
|
||||
.topbar-item:visited,
|
||||
a.topbar-item,
|
||||
a.topbar-item:visited,
|
||||
.dropdown-button,
|
||||
.dropdown-menu {
|
||||
background: #000A89;
|
||||
div.dropdown-menu {
|
||||
background: #080e5e;
|
||||
color: #EEF2FF;
|
||||
}
|
||||
|
||||
div#footer {
|
||||
font-size: 8pt;
|
||||
div.dropdown-menu {
|
||||
-webkit-filter: drop-shadow(3px 5px 6px #555555);
|
||||
filter: drop-shadow(3px 5px 6px #555555);
|
||||
}
|
||||
div.dropdown-menu a, div.dropdown-menu h3 {
|
||||
color: #EEF2FF;
|
||||
}
|
||||
|
||||
a.topbar-item:hover,
|
||||
a.topbar-item:active,
|
||||
div.dropdown-menu a:hover {
|
||||
background: #0a127b;
|
||||
}
|
||||
|
||||
footer {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ table.mgmt-table tr:first-of-type th {
|
|||
}
|
||||
|
||||
span.subject {
|
||||
font-weight: bold;
|
||||
font-weight: 800;
|
||||
color: #34ED3A;
|
||||
}
|
||||
|
||||
|
@ -71,3 +71,13 @@ div.section-title-block {
|
|||
background: #A7A7A7;
|
||||
border-bottom: 1px solid #117743;
|
||||
}
|
||||
|
||||
div#upload-box {
|
||||
background: #aaa;
|
||||
}
|
||||
div#upload-box a {
|
||||
color: #444;
|
||||
}
|
||||
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ body {
|
|||
background: #1E1E1E;
|
||||
font-family: sans-serif;
|
||||
color: #999;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -56,6 +55,13 @@ span.tripcode {
|
|||
color: #32DD72;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background: #666 !important;
|
||||
}
|
||||
.dropdown-menu b {
|
||||
color: black;
|
||||
}
|
||||
|
||||
span.subject {
|
||||
color: #446655;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,23 @@
|
|||
@keyframes slideopen {
|
||||
from {
|
||||
transform: scale(1, 0);
|
||||
transform-origin: top center;
|
||||
}
|
||||
to {
|
||||
transform: scale(1, 1);
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
@keyframes slideclose {
|
||||
from {
|
||||
transform: scale(1, 1);
|
||||
transform-origin: top center;
|
||||
}
|
||||
to {
|
||||
transform: scale(1, 0);
|
||||
transform-origin: top center;
|
||||
}
|
||||
}
|
||||
#boardmenu-bottom {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
@ -220,6 +240,32 @@ div.inlinepostprev {
|
|||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.hideblock {
|
||||
border: 1px solid black;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hideblock.open {
|
||||
animation: slideopen 0.25s;
|
||||
}
|
||||
|
||||
.hideblock.close {
|
||||
animation: slideclose 0.25s;
|
||||
}
|
||||
|
||||
.hideblock.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hideblock-button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #aaa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.section-block {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
@ -439,19 +485,17 @@ form#login-box input[type=submit] {
|
|||
}
|
||||
|
||||
.lightbox-title {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.lightbox-x {
|
||||
color: #000 !important;
|
||||
float: right;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.lightbox-x:hover {
|
||||
.lightbox-x:hover, .lightbox-x:active {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
|
@ -506,7 +550,7 @@ div#qr-box {
|
|||
}
|
||||
div#qr-box input[type=text], div#qr-box textarea {
|
||||
display: table-cell;
|
||||
min-width: 300px;
|
||||
min-width: 320px;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
width: 100%;
|
||||
|
@ -597,6 +641,10 @@ img#banpage-image {
|
|||
margin: 4px 8px 8px 4px;
|
||||
}
|
||||
|
||||
.increase-line-height header, .increase-line-height .post, .increase-line-height .reply {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
|
@ -668,7 +716,7 @@ div.section-block div.section-body {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#footer {
|
||||
footer {
|
||||
bottom: 0px;
|
||||
clear: both;
|
||||
left: 0px;
|
||||
|
|
|
@ -65,3 +65,13 @@ header, div#top-pane, a {
|
|||
#site-title, #board-title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
div#upload-box {
|
||||
background: #aaa;
|
||||
}
|
||||
div#upload-box a {
|
||||
color: #444;
|
||||
}
|
||||
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
|
|
@ -121,7 +121,7 @@ a.topbar-item:hover {
|
|||
background: #404040;
|
||||
}
|
||||
|
||||
div#footer, div#footer * {
|
||||
footer, footer * {
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
|
|
|
@ -93,3 +93,17 @@ span.subject {
|
|||
color: #CC1105;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #222;
|
||||
}
|
||||
|
||||
div#upload-box {
|
||||
background: #aaa;
|
||||
}
|
||||
div#upload-box a {
|
||||
color: #444;
|
||||
}
|
||||
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
|
|
@ -93,3 +93,17 @@ span.subject {
|
|||
color: #0F0C5D;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dice-roll {
|
||||
border: 1px dashed #222;
|
||||
}
|
||||
|
||||
div#upload-box {
|
||||
background: #aaa;
|
||||
}
|
||||
div#upload-box a {
|
||||
color: #444;
|
||||
}
|
||||
div#upload-box a:hover, div#upload-box a:target, div#upload-box a:focus {
|
||||
color: #666;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
<h1>404: File not found</h1>
|
||||
<img src="/error/lol 404.gif" alt="lol 404">
|
||||
<p>The requested file could not be found on this server.</p>
|
||||
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.0.1
|
||||
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
|
||||
</body>
|
||||
</html>
|
|
@ -7,6 +7,6 @@
|
|||
<h1>Error 500: Internal Server error</h1>
|
||||
<img src="/error/server500.gif" alt="server burning">
|
||||
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon they get around to it, whenever that is. Hopefully soon.</p>
|
||||
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.0.1
|
||||
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
|
||||
</body>
|
||||
</html>
|
|
@ -7,6 +7,6 @@
|
|||
<h1>Error 502: Bad gateway</h1>
|
||||
<img src="/error/server500.gif" alt="server burning">
|
||||
<p>The server encountered an error while trying to serve the page, and we apologize for the inconvenience. The <a href="https://en.wikipedia.org/wiki/Idiot">system administrator</a> will try to fix things as soon they get around to it, whenever that is. Hopefully soon.</p>
|
||||
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.0.1
|
||||
<hr/>Site powered by <a href="https://github.com/gochan-org/gochan" target="_blank">Gochan</a> v4.1.0
|
||||
</body>
|
||||
</html>
|
Before Width: | Height: | Size: 753 B After Width: | Height: | Size: 753 B |
|
@ -7,11 +7,11 @@
|
|||
viewBox="0 0 120 120"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:export-filename="cyclical.png"
|
||||
inkscape:export-filename="cyclic.png"
|
||||
inkscape:export-xdpi="12.8"
|
||||
inkscape:export-ydpi="12.8"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
sodipodi:docname="cyclical.svg"
|
||||
sodipodi:docname="cyclic.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
@ -21,7 +21,7 @@ import (
|
|||
|
||||
const (
|
||||
dirIsAFileStr = `unable to create %q, path exists and is a file`
|
||||
genericErrStr = `unable to create %q: %s`
|
||||
genericErrStr = `unable to create %q: %w`
|
||||
pathExistsStr = `unable to create %q, path already exists`
|
||||
)
|
||||
|
||||
|
@ -63,12 +63,12 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Failed getting board threads")
|
||||
return fmt.Errorf("error getting threads for /%s/: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("error getting threads for /%s/: %w", board.Dir, err)
|
||||
}
|
||||
topPosts, err := getBoardTopPosts(board.Dir)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed getting board threads")
|
||||
return fmt.Errorf("error getting OP posts for /%s/: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("error getting OP posts for /%s/: %w", board.Dir, err)
|
||||
}
|
||||
opMap := make(map[int]*Post)
|
||||
for _, post := range topPosts {
|
||||
|
@ -102,13 +102,13 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
catalogThread.Replies, err = thread.GetReplyCount()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed getting reply count")
|
||||
return errors.New("Error getting reply count: " + err.Error())
|
||||
return fmt.Errorf("error getting reply count: %w", err)
|
||||
}
|
||||
|
||||
catalogThread.Posts, err = getThreadPosts(&thread)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed getting replies")
|
||||
return errors.New("Failed getting replies: " + err.Error())
|
||||
return fmt.Errorf("failed getting replies: %w", err)
|
||||
}
|
||||
if len(catalogThread.Posts) == 0 {
|
||||
continue
|
||||
|
@ -122,7 +122,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
catalogThread.uploads, err = thread.GetUploads()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Msg("Failed getting thread uploads")
|
||||
return errors.New("Failed getting thread uploads: " + err.Error())
|
||||
return fmt.Errorf("failed getting thread uploads: %w", err)
|
||||
}
|
||||
|
||||
var imagesOnBoardPage int
|
||||
|
@ -157,19 +157,19 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
errEv.Err(err).Caller().
|
||||
Str("page", "board.html").
|
||||
Msg("Failed getting board page")
|
||||
return fmt.Errorf("failed opening /%s/board.html: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("failed opening /%s/board.html: %w", board.Dir, err)
|
||||
}
|
||||
defer boardPageFile.Close()
|
||||
|
||||
if err = config.TakeOwnershipOfFile(boardPageFile); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Unable to take ownership of board.html")
|
||||
return fmt.Errorf("unable to take ownership of /%s/board.html: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("unable to take ownership of /%s/board.html: %w", board.Dir, err)
|
||||
}
|
||||
// Render board page template to the file,
|
||||
// packaging the board/section list, threads, and board info
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]interface{}{
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, map[string]any{
|
||||
"boards": gcsql.AllBoards,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": threads,
|
||||
|
@ -183,7 +183,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
errEv.Err(err).Caller().
|
||||
Str("page", "board.html").
|
||||
Msg("Failed building board")
|
||||
return fmt.Errorf("failed building /%s/: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("failed building /%s/: %w", board.Dir, err)
|
||||
}
|
||||
|
||||
if err = boardPageFile.Close(); err != nil {
|
||||
|
@ -207,14 +207,14 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
if err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Failed opening catalog.json")
|
||||
return fmt.Errorf("failed opening /%s/catalog.json: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("failed opening /%s/catalog.json: %w", board.Dir, err)
|
||||
}
|
||||
defer catalogJSONFile.Close()
|
||||
|
||||
if err = config.TakeOwnershipOfFile(catalogJSONFile); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Msg("Unable to take ownership of catalog.json")
|
||||
return fmt.Errorf("unable to take ownership of /%s/catalog.json: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("unable to take ownership of /%s/catalog.json: %w", board.Dir, err)
|
||||
}
|
||||
for _, page := range catalog.pages {
|
||||
catalog.currentPage++
|
||||
|
@ -243,7 +243,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
if (numThreads % boardConfig.ThreadsPerPage) > 0 {
|
||||
numPages++
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
data := map[string]any{
|
||||
"boards": gcsql.AllBoards,
|
||||
"sections": gcsql.AllSections,
|
||||
"threads": page.Threads,
|
||||
|
@ -262,7 +262,7 @@ func BuildBoardPages(board *gcsql.Board, errEv *zerolog.Event) error {
|
|||
}
|
||||
if err = serverutil.MinifyTemplate(gctemplates.BoardPage, data, currentPageFile, "text/html"); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("failed building /%s/ boardpage: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("failed building /%s/ boardpage: %w", board.Dir, err)
|
||||
}
|
||||
if err = currentPageFile.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
|
@ -307,7 +307,7 @@ func BuildBoards(verbose bool, which ...int) error {
|
|||
errEv.Err(err).Caller().
|
||||
Int("boardid", boardID).
|
||||
Msg("Unable to get board information")
|
||||
return fmt.Errorf("unable to get board information (ID: %d): %s", boardID, err.Error())
|
||||
return fmt.Errorf("unable to get board information (ID: %d): %w", boardID, err)
|
||||
}
|
||||
boards = append(boards, *board)
|
||||
}
|
||||
|
@ -446,12 +446,12 @@ func buildBoard(board *gcsql.Board, force bool) error {
|
|||
} else if err = os.Mkdir(dirPath, config.DirFileMode); err != nil {
|
||||
errEv.Err(os.ErrExist).Caller().
|
||||
Str("dirPath", dirPath).Send()
|
||||
return fmt.Errorf(genericErrStr, dirPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, dirPath, err)
|
||||
}
|
||||
if err = config.TakeOwnership(dirPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("dirPath", dirPath).Send()
|
||||
return fmt.Errorf(genericErrStr, dirPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, dirPath, err)
|
||||
}
|
||||
|
||||
if resInfo != nil {
|
||||
|
@ -468,15 +468,15 @@ func buildBoard(board *gcsql.Board, force bool) error {
|
|||
return err
|
||||
}
|
||||
} else if err = os.Mkdir(resPath, config.DirFileMode); err != nil {
|
||||
err = fmt.Errorf(genericErrStr, resPath, err.Error())
|
||||
err = fmt.Errorf(genericErrStr, resPath, err)
|
||||
errEv.Err(err).Caller().
|
||||
Str("resPath", resPath).Send()
|
||||
return fmt.Errorf(genericErrStr, resPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, resPath, err)
|
||||
}
|
||||
if err = config.TakeOwnership(resPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("resPath", resPath).Send()
|
||||
return fmt.Errorf(genericErrStr, resPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, resPath, err)
|
||||
}
|
||||
|
||||
if srcInfo != nil {
|
||||
|
@ -493,7 +493,7 @@ func buildBoard(board *gcsql.Board, force bool) error {
|
|||
return err
|
||||
}
|
||||
} else if err = os.Mkdir(srcPath, config.DirFileMode); err != nil {
|
||||
err = fmt.Errorf(genericErrStr, srcPath, err.Error())
|
||||
err = fmt.Errorf(genericErrStr, srcPath, err)
|
||||
errEv.Err(err).Caller().
|
||||
Str("srcPath", srcPath).Send()
|
||||
return err
|
||||
|
@ -501,7 +501,7 @@ func buildBoard(board *gcsql.Board, force bool) error {
|
|||
if config.TakeOwnership(srcPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("srcPath", srcPath).Send()
|
||||
return fmt.Errorf(genericErrStr, srcPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, srcPath, err)
|
||||
}
|
||||
|
||||
if thumbInfo != nil {
|
||||
|
@ -514,12 +514,12 @@ func buildBoard(board *gcsql.Board, force bool) error {
|
|||
} else if err = os.Mkdir(thumbPath, config.DirFileMode); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).Send()
|
||||
return fmt.Errorf(genericErrStr, thumbPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, thumbPath, err)
|
||||
}
|
||||
if config.TakeOwnership(thumbPath); err != nil {
|
||||
errEv.Err(err).Caller().
|
||||
Str("thumbPath", thumbPath).Send()
|
||||
return fmt.Errorf(genericErrStr, thumbPath, err.Error())
|
||||
return fmt.Errorf(genericErrStr, thumbPath, err)
|
||||
}
|
||||
|
||||
if err = BuildBoardPages(board, errEv); err != nil {
|
||||
|
@ -562,12 +562,12 @@ func BuildBoardListJSON() error {
|
|||
defer errEv.Discard()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("unable to open boards.json for writing: " + err.Error())
|
||||
return fmt.Errorf("unable to open boards.json for writing: %w", err)
|
||||
}
|
||||
|
||||
if err = config.TakeOwnershipOfFile(boardListFile); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("unable to update boards.json ownership: " + err.Error())
|
||||
return fmt.Errorf("unable to update boards.json ownership: %w", err)
|
||||
}
|
||||
|
||||
boardsListJSONData := boardsListJSON{
|
||||
|
@ -585,15 +585,15 @@ func BuildBoardListJSON() error {
|
|||
boardJSON, err := json.Marshal(boardsListJSONData)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Failed to create boards.json " + err.Error())
|
||||
return fmt.Errorf("failed to create boards.json: %w", err)
|
||||
}
|
||||
|
||||
if _, err = serverutil.MinifyWriter(boardListFile, boardJSON, "application/json"); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
errEv.Err(err).Caller().Msg("Failed writing to boards.json")
|
||||
return errors.New("failed writing boards.json file")
|
||||
}
|
||||
if err = boardListFile.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
errEv.Err(err).Caller().Msg("Failed closing boards.json")
|
||||
return errors.New("failed closing boards.json")
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package building
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
@ -36,9 +35,9 @@ func getFrontPagePosts() ([]frontPagePost, error) {
|
|||
|
||||
if siteCfg.RecentPostsWithNoFile {
|
||||
// get recent posts, including those with no file
|
||||
query = "SELECT * FROM DBPREFIXv_front_page_posts"
|
||||
query = "SELECT id, message_raw, dir, filename, op_id FROM DBPREFIXv_front_page_posts"
|
||||
} else {
|
||||
query = "SELECT * FROM DBPREFIXv_front_page_posts_with_file"
|
||||
query = "SELECT id, message_raw, dir, filename, op_id FROM DBPREFIXv_front_page_posts_with_file"
|
||||
}
|
||||
query += " ORDER BY id DESC LIMIT " + strconv.Itoa(siteCfg.MaxRecentPosts)
|
||||
|
||||
|
@ -84,18 +83,18 @@ func BuildFrontPage() error {
|
|||
err := gctemplates.InitTemplates(gctemplates.FrontPage)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Error loading front page template: " + err.Error())
|
||||
return fmt.Errorf("failed loading front page template: %w", err)
|
||||
}
|
||||
criticalCfg := config.GetSystemCriticalConfig()
|
||||
frontFile, err := os.OpenFile(path.Join(criticalCfg.DocumentRoot, "index.html"), os.O_CREATE|os.O_RDWR|os.O_TRUNC, config.NormalFileMode)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Failed opening front page for writing: " + err.Error())
|
||||
return fmt.Errorf("failed opening front page for writing: %w", err)
|
||||
}
|
||||
|
||||
if err = config.TakeOwnershipOfFile(frontFile); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Failed setting file ownership for front page: " + err.Error())
|
||||
return fmt.Errorf("failed setting file ownership for front page: %w", err)
|
||||
}
|
||||
|
||||
var recentPostsArr []frontPagePost
|
||||
|
@ -103,9 +102,9 @@ func BuildFrontPage() error {
|
|||
recentPostsArr, err = getFrontPagePosts()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Failed loading recent posts: " + err.Error())
|
||||
return fmt.Errorf("failed loading recent posts: %w", err)
|
||||
}
|
||||
if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]interface{}{
|
||||
if err = serverutil.MinifyTemplate(gctemplates.FrontPage, map[string]any{
|
||||
"siteConfig": siteCfg,
|
||||
"sections": gcsql.AllSections,
|
||||
"boards": gcsql.AllBoards,
|
||||
|
@ -113,15 +112,15 @@ func BuildFrontPage() error {
|
|||
"recentPosts": recentPostsArr,
|
||||
}, frontFile, "text/html"); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Failed executing front page template: " + err.Error())
|
||||
return fmt.Errorf("failed executing front page template: %w", err)
|
||||
}
|
||||
return frontFile.Close()
|
||||
}
|
||||
|
||||
// BuildPageHeader is a convenience function for automatically generating the top part
|
||||
// of every normal HTML page
|
||||
func BuildPageHeader(writer io.Writer, pageTitle string, board string, misc map[string]interface{}) error {
|
||||
phMap := map[string]interface{}{
|
||||
func BuildPageHeader(writer io.Writer, pageTitle string, board string, misc map[string]any) error {
|
||||
phMap := map[string]any{
|
||||
"pageTitle": pageTitle,
|
||||
"documentTitle": pageTitle + " - " + config.GetSiteConfig().SiteName,
|
||||
"siteConfig": config.GetSiteConfig(),
|
||||
|
@ -139,7 +138,7 @@ func BuildPageHeader(writer io.Writer, pageTitle string, board string, misc map[
|
|||
// of every normal HTML page
|
||||
func BuildPageFooter(writer io.Writer) (err error) {
|
||||
return serverutil.MinifyTemplate(gctemplates.PageFooter,
|
||||
map[string]interface{}{}, writer, "text/html")
|
||||
map[string]any{}, writer, "text/html")
|
||||
}
|
||||
|
||||
// BuildJS minifies (if enabled) consts.js, which is built from a template
|
||||
|
@ -150,7 +149,7 @@ func BuildJS() error {
|
|||
defer errEv.Discard()
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return errors.New("Error loading consts.js template:" + err.Error())
|
||||
return fmt.Errorf("failed loading consts.js template: %w", err)
|
||||
}
|
||||
|
||||
boardCfg := config.GetBoardConfig("")
|
||||
|
@ -159,12 +158,12 @@ func BuildJS() error {
|
|||
constsJSFile, err := os.OpenFile(constsJSPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, config.NormalFileMode)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("error opening consts.js for writing: %s", err.Error())
|
||||
return fmt.Errorf("failed opening consts.js for writing: %w", err)
|
||||
}
|
||||
|
||||
if err = config.TakeOwnershipOfFile(constsJSFile); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("unable to update file ownership for consts.js: %s", err.Error())
|
||||
return fmt.Errorf("unable to update file ownership for consts.js: %w", err)
|
||||
}
|
||||
|
||||
if err = serverutil.MinifyTemplate(gctemplates.JsConsts, map[string]any{
|
||||
|
@ -175,7 +174,7 @@ func BuildJS() error {
|
|||
"fileTypes": boardCfg.AllowOtherExtensions,
|
||||
}, constsJSFile, "text/javascript"); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("error building consts.js: %s", err.Error())
|
||||
return fmt.Errorf("failed building consts.js: %w", err)
|
||||
}
|
||||
return constsJSFile.Close()
|
||||
}
|
||||
|
|
|
@ -25,14 +25,14 @@ func TestBuildJS(t *testing.T) {
|
|||
}
|
||||
|
||||
outDir := t.TempDir()
|
||||
config.SetVersion("3.11.0")
|
||||
config.SetVersion("4.0.2")
|
||||
systemCriticalCfg := config.GetSystemCriticalConfig()
|
||||
systemCriticalCfg.DocumentRoot = path.Join(outDir, "html")
|
||||
systemCriticalCfg.TemplateDir = path.Join(testRoot, "templates")
|
||||
systemCriticalCfg.LogDir = path.Join(outDir, "logs")
|
||||
systemCriticalCfg.WebRoot = "/chan"
|
||||
systemCriticalCfg.TimeZone = 8
|
||||
config.SetSystemCriticalConfig(&systemCriticalCfg)
|
||||
config.SetSystemCriticalConfig(systemCriticalCfg)
|
||||
|
||||
boardCfg := config.GetBoardConfig("")
|
||||
boardCfg.Styles = []config.Style{
|
||||
|
@ -109,7 +109,7 @@ func doFrontBuildingTest(t *testing.T, mock sqlmock.Sqlmock, expectOut string) {
|
|||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "abbreviation", "position", "hidden"}).
|
||||
AddRows([]driver.Value{1, "Main", "main", 1, false}))
|
||||
|
||||
mock.ExpectPrepare(`SELECT \* FROM v_front_page_posts_with_file ORDER BY id DESC LIMIT 15`).ExpectQuery().WillReturnRows(
|
||||
mock.ExpectPrepare(`SELECT id, message_raw, dir, filename, op_id FROM v_front_page_posts_with_file ORDER BY id DESC LIMIT 15`).ExpectQuery().WillReturnRows(
|
||||
sqlmock.NewRows([]string{"posts.id", "posts.message_raw", "dir", "filename", "op.id"}).
|
||||
AddRows(
|
||||
[]driver.Value{1, "message_raw", "test", "filename", 1},
|
||||
|
@ -147,14 +147,14 @@ func TestBuildFrontPage(t *testing.T) {
|
|||
}
|
||||
t.Run(driver, func(t *testing.T) {
|
||||
outDir := t.TempDir()
|
||||
config.SetVersion("3.11.0")
|
||||
config.SetVersion("4.0.2")
|
||||
systemCriticalCfg := config.GetSystemCriticalConfig()
|
||||
systemCriticalCfg.DocumentRoot = path.Join(outDir, "html")
|
||||
systemCriticalCfg.TemplateDir = path.Join(testRoot, "templates")
|
||||
systemCriticalCfg.LogDir = path.Join(outDir, "logs")
|
||||
systemCriticalCfg.WebRoot = "/chan"
|
||||
systemCriticalCfg.TimeZone = 8
|
||||
config.SetSystemCriticalConfig(&systemCriticalCfg)
|
||||
config.SetSystemCriticalConfig(systemCriticalCfg)
|
||||
|
||||
boardCfg := config.GetBoardConfig("")
|
||||
boardCfg.Styles = []config.Style{{Name: "test1", Filename: "test1.css"}}
|
||||
|
|
|
@ -7,7 +7,7 @@ const defaultStyle = "test1.css";
|
|||
const webroot = "/chan";
|
||||
const serverTZ = 8;
|
||||
const fileTypes = [];`
|
||||
expectedMinifiedFront = `<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Gochan</title><link rel=stylesheet href=/chan/css/global.css><link id=theme rel=stylesheet href=/chan/css/test1.css><link rel="shortcut icon" href=/chan/favicon.png><script src=/chan/js/consts.js></script><script src=/chan/js/gochan.js></script><div id=topbar><div class=topbar-section><a href=/chan/ class=topbar-item>home</a></div><div class=topbar-section><a href=/chan/test/ class=topbar-item title="Testing board">/test/</a><a href=/chan/test2/ class=topbar-item title="Testing board 2">/test2/</a></div></div><div id=content><div id=top-pane><h1 id=site-title>Gochan</h1><span id=site-slogan></span></div><br><div id=frontpage><div class=section-block style="margin: 16px 64px 16px 64px;"><div class="section-body front-intro">Welcome to Gochan!</div></div><div class=section-block><div class=section-title-block><b>Boards</b></div><div class=section-body><ul style="float:left; list-style: none"><li style="text-align: center; font-weight: bold"><b><u>Main</u></b><li><a href=/chan/test/ title="Board for testing description">/test/</a> — Testing board<li><a href=/chan/test2/ title="Board for testing description 2">/test2/</a> — Testing board 2</ul></div></div><div class=section-block><div class=section-title-block><b>Recent Posts</b></div><div class=section-body><div id=recent-posts><div class=recent-post><a href=/chan/test/res/1.html#1 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div><div class=recent-post><a href=/chan/test/res/1.html#2 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div></div></div></div></div><div id=footer>Powered by <a href=http://github.com/gochan-org/gochan/>Gochan 3.11</a><br></div></div>`
|
||||
expectedMinifiedFront = `<!doctype html><html lang=en><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Gochan</title><link rel=stylesheet href=/chan/css/global.css><link id=theme rel=stylesheet href=/chan/css/test1.css><link rel="shortcut icon" href=/chan/favicon.png><script src=/chan/js/consts.js></script><script src=/chan/js/gochan.js defer></script><div id=topbar><div class=topbar-section><a href=/chan/ class=topbar-item>home</a></div><div class=topbar-section><a href=/chan/test/ class=topbar-item title="Testing board">/test/</a><a href=/chan/test2/ class=topbar-item title="Testing board 2">/test2/</a></div></div><div id=content><div id=top-pane><h1 id=site-title>Gochan</h1><span id=site-slogan></span></div><br><div id=frontpage><div class=section-block style="margin: 16px 64px 16px 64px;"><div class="section-body front-intro">Welcome to Gochan!</div></div><div class=section-block><div class=section-title-block><b>Boards</b></div><div class=section-body><ul style="float:left; list-style: none"><li style="text-align: center; font-weight: bold"><b><u>Main</u></b><li><a href=/chan/test/ title="Board for testing description">/test/</a> — Testing board<li><a href=/chan/test2/ title="Board for testing description 2">/test2/</a> — Testing board 2</ul></div></div><div class=section-block><div class=section-title-block><b>Recent Posts</b></div><div class=section-body><div id=recent-posts><div class=recent-post><a href=/chan/test/res/1.html#1 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div><div class=recent-post><a href=/chan/test/res/1.html#2 class=front-reply target=_blank><img src=/chan/test/thumb alt="post thumbnail"></a><br><br><a href=/chan/test/>/test/</a><hr>message_raw</div></div></div></div></div><footer>Powered by <a href=http://github.com/gochan-org/gochan/>Gochan 4.0.2</a><br></footer></div>`
|
||||
expectedUnminifiedFront = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
@ -17,7 +17,7 @@ const fileTypes = [];`
|
|||
<link rel="stylesheet" href="/chan/css/global.css" />
|
||||
<link id="theme" rel="stylesheet" href="/chan/css/test1.css" />
|
||||
<link rel="shortcut icon" href="/chan/favicon.png"><script type="text/javascript" src="/chan/js/consts.js"></script>
|
||||
<script type="text/javascript" src="/chan/js/gochan.js"></script>
|
||||
<script type="text/javascript" src="/chan/js/gochan.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="topbar">
|
||||
|
@ -73,9 +73,9 @@ const fileTypes = [];`
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
Powered by <a href="http://github.com/gochan-org/gochan/">Gochan 3.11</a><br />
|
||||
</div>
|
||||
<footer>
|
||||
Powered by <a href="http://github.com/gochan-org/gochan/">Gochan 4.0.2</a><br />
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -63,7 +63,10 @@ func (catalog *boardCatalog) fillPages(threadsPerPage int, threads []catalogThre
|
|||
}
|
||||
|
||||
func getBoardTopPosts(board string) ([]*Post, error) {
|
||||
const query = "SELECT * FROM DBPREFIXv_building_posts WHERE id = parent_id AND dir = ?"
|
||||
const query = `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
|
||||
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
|
||||
width, height, locked, stickied, cyclical, flag, country, is_deleted
|
||||
FROM DBPREFIXv_building_posts WHERE id = parent_id AND dir = ?`
|
||||
var posts []*Post
|
||||
|
||||
err := QueryPosts(query, []any{board}, func(p *Post) error {
|
||||
|
@ -95,7 +98,7 @@ func BuildCatalog(boardID int) error {
|
|||
catalogFile, err := os.OpenFile(catalogPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, config.NormalFileMode)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("failed opening /%s/catalog.html: %s", board.Dir, err.Error())
|
||||
return fmt.Errorf("failed opening /%s/catalog.html: %w", board.Dir, err)
|
||||
}
|
||||
|
||||
if err = config.TakeOwnershipOfFile(catalogFile); err != nil {
|
||||
|
@ -110,7 +113,7 @@ func BuildCatalog(boardID int) error {
|
|||
}
|
||||
boardConfig := config.GetBoardConfig(board.Dir)
|
||||
|
||||
if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]interface{}{
|
||||
if err = serverutil.MinifyTemplate(gctemplates.Catalog, map[string]any{
|
||||
"boards": gcsql.AllBoards,
|
||||
"board": board,
|
||||
"boardConfig": boardConfig,
|
||||
|
|
|
@ -99,8 +99,8 @@ func (p *Post) Stickied() bool {
|
|||
return p.thread.Stickied
|
||||
}
|
||||
|
||||
func (p *Post) Cyclical() bool {
|
||||
return p.thread.Cyclical
|
||||
func (p *Post) Cyclic() bool {
|
||||
return p.thread.Cyclic
|
||||
}
|
||||
|
||||
// Select all from v_building_posts (and queries with the same columns) and call the callback function on each Post
|
||||
|
@ -131,7 +131,7 @@ func QueryPosts(query string, params []any, cb func(*Post) error) error {
|
|||
&post.LastModified, &post.ParentID, &lastBump, &post.Message, &post.MessageRaw, &post.BoardID,
|
||||
&post.BoardDir, &post.OriginalFilename, &post.Filename, &post.Checksum, &post.Filesize,
|
||||
&post.ThumbnailWidth, &post.ThumbnailHeight, &post.UploadWidth, &post.UploadHeight,
|
||||
&post.thread.Locked, &post.thread.Stickied, &post.thread.Cyclical, &post.Country.Flag, &post.Country.Name,
|
||||
&post.thread.Locked, &post.thread.Stickied, &post.thread.Cyclic, &post.Country.Flag, &post.Country.Name,
|
||||
&post.IsDeleted)
|
||||
|
||||
if err = rows.Scan(dest...); err != nil {
|
||||
|
@ -155,7 +155,10 @@ func QueryPosts(query string, params []any, cb func(*Post) error) error {
|
|||
}
|
||||
|
||||
func GetBuildablePostsByIP(ip string, limit int) ([]*Post, error) {
|
||||
query := "SELECT * FROM DBPREFIXv_building_posts WHERE ip = PARAM_ATON ORDER BY id DESC"
|
||||
query := `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
|
||||
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
|
||||
width, height, locked, stickied, cyclical, flag, country, is_deleted
|
||||
FROM DBPREFIXv_building_posts WHERE ip = PARAM_ATON ORDER BY id DESC`
|
||||
if limit > 0 {
|
||||
query += " LIMIT " + strconv.Itoa(limit)
|
||||
}
|
||||
|
@ -169,7 +172,10 @@ func GetBuildablePostsByIP(ip string, limit int) ([]*Post, error) {
|
|||
}
|
||||
|
||||
func getThreadPosts(thread *gcsql.Thread) ([]*Post, error) {
|
||||
const query = "SELECT * FROM DBPREFIXv_building_posts WHERE thread_id = ? ORDER BY id ASC"
|
||||
const query = `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
|
||||
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
|
||||
width, height, locked, stickied, cyclical, flag, country, is_deleted
|
||||
FROM DBPREFIXv_building_posts WHERE thread_id = ? ORDER BY id ASC`
|
||||
var posts []*Post
|
||||
err := QueryPosts(query, []any{thread.ID}, func(p *Post) error {
|
||||
posts = append(posts, p)
|
||||
|
@ -179,7 +185,10 @@ func getThreadPosts(thread *gcsql.Thread) ([]*Post, error) {
|
|||
}
|
||||
|
||||
func GetRecentPosts(boardid int, limit int) ([]*Post, error) {
|
||||
query := `SELECT * FROM DBPREFIXv_building_posts`
|
||||
query := `SELECT id, thread_id, ip, name, tripcode, email, subject, created_on, last_modified, parent_id,
|
||||
last_bump, message, message_raw, board_id, dir, original_filename, filename, checksum, filesize, tw, th,
|
||||
width, height, locked, stickied, cyclical, flag, country, is_deleted
|
||||
FROM DBPREFIXv_building_posts`
|
||||
var args []any
|
||||
|
||||
if boardid > 0 {
|
||||
|
|
|
@ -84,18 +84,18 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
threadPageFile, err = os.OpenFile(threadPageFilepath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, config.NormalFileMode)
|
||||
if err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("unable to open /%s/res/%d.html: %s", board.Dir, op.ID, err.Error())
|
||||
return fmt.Errorf("unable to open /%s/res/%d.html: %w", board.Dir, op.ID, err)
|
||||
}
|
||||
|
||||
if err = config.TakeOwnershipOfFile(threadPageFile); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("unable to set file permissions for /%s/res/%d.html: %s", board.Dir, op.ID, err.Error())
|
||||
return fmt.Errorf("unable to set file permissions for /%s/res/%d.html: %w", board.Dir, op.ID, err)
|
||||
}
|
||||
errEv.Int("op", posts[0].ID)
|
||||
|
||||
// render thread page
|
||||
captchaCfg := config.GetSiteConfig().Captcha
|
||||
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]interface{}{
|
||||
if err = serverutil.MinifyTemplate(gctemplates.ThreadPage, map[string]any{
|
||||
"boards": gcsql.AllBoards,
|
||||
"board": board,
|
||||
"boardConfig": config.GetBoardConfig(board.Dir),
|
||||
|
@ -107,7 +107,7 @@ func BuildThreadPages(op *gcsql.Post) error {
|
|||
"captcha": captchaCfg,
|
||||
}, threadPageFile, "text/html"); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
return fmt.Errorf("failed building /%s/res/%d threadpage: %s", board.Dir, posts[0].ID, err.Error())
|
||||
return fmt.Errorf("failed building /%s/res/%d threadpage: %w", board.Dir, posts[0].ID, err)
|
||||
}
|
||||
if err = threadPageFile.Close(); err != nil {
|
||||
errEv.Err(err).Caller().Send()
|
||||
|
|
|
@ -41,11 +41,15 @@ type GochanConfig struct {
|
|||
// ValidateValues checks to make sure that the configuration options are usable
|
||||
// (e.g., ListenIP is a valid IP address, Port isn't a negative number, etc)
|
||||
func (gcfg *GochanConfig) ValidateValues() error {
|
||||
// if net.ParseIP(gcfg.ListenIP) == nil {
|
||||
// return &InvalidValueError{Field: "ListenIP", Value: gcfg.ListenIP}
|
||||
// }
|
||||
changed := false
|
||||
|
||||
if gcfg.SiteDomain == "" {
|
||||
return &InvalidValueError{Field: "SiteDomain", Value: gcfg.SiteDomain, Details: "must be set"}
|
||||
}
|
||||
if strings.Contains(gcfg.SiteDomain, " ") || strings.Contains(gcfg.SiteDomain, "://") {
|
||||
return &InvalidValueError{Field: "SiteDomain", Value: gcfg.SiteDomain, Details: "must be a host (port optional)"}
|
||||
}
|
||||
|
||||
_, err := durationutil.ParseLongerDuration(gcfg.CookieMaxAge)
|
||||
if errors.Is(err, durationutil.ErrInvalidDurationString) {
|
||||
return &InvalidValueError{Field: "CookieMaxAge", Value: gcfg.CookieMaxAge, Details: err.Error() + cookieMaxAgeEx}
|
||||
|
@ -162,10 +166,11 @@ type SystemCriticalConfig struct {
|
|||
|
||||
SQLConfig
|
||||
|
||||
Verbose bool `json:"DebugMode"`
|
||||
RandomSeed string
|
||||
Version *GochanVersion `json:"-"`
|
||||
TimeZone int `json:"-"`
|
||||
CheckRequestReferer bool
|
||||
Verbose bool `json:"DebugMode"`
|
||||
RandomSeed string
|
||||
Version *GochanVersion `json:"-"`
|
||||
TimeZone int `json:"-"`
|
||||
}
|
||||
|
||||
// SiteConfig contains information about the site/community, e.g. the name of the site, the slogan (if set),
|
||||
|
@ -321,7 +326,8 @@ type PostConfig struct {
|
|||
RepliesOnBoardPage int
|
||||
StickyRepliesOnBoardPage int
|
||||
NewThreadsRequireUpload bool
|
||||
CyclicalThreadNumPosts int
|
||||
EnableCyclicThreads bool
|
||||
CyclicThreadNumPosts int
|
||||
|
||||
BanColors []string
|
||||
BanMessage string
|
||||
|
@ -331,6 +337,7 @@ type PostConfig struct {
|
|||
ImagesOpenNewTab bool
|
||||
NewTabOnOutlinks bool
|
||||
DisableBBcode bool
|
||||
AllowDiceRerolls bool
|
||||
}
|
||||
|
||||
func WriteConfig() error {
|
||||
|
@ -345,8 +352,8 @@ func GetSQLConfig() SQLConfig {
|
|||
|
||||
// GetSystemCriticalConfig returns system-critical configuration options like listening IP
|
||||
// It returns a value instead of a pointer, because it is not usually safe to edit while Gochan is running.
|
||||
func GetSystemCriticalConfig() SystemCriticalConfig {
|
||||
return cfg.SystemCriticalConfig
|
||||
func GetSystemCriticalConfig() *SystemCriticalConfig {
|
||||
return &cfg.SystemCriticalConfig
|
||||
}
|
||||
|
||||
// GetSiteConfig returns the global site configuration (site name, slogan, etc)
|
||||
|
|
|
@ -10,6 +10,7 @@ var (
|
|||
DBMaxIdleConnections: DefaultSQLMaxConns,
|
||||
DBConnMaxLifetimeMin: DefaultSQLConnMaxLifetimeMin,
|
||||
},
|
||||
CheckRequestReferer: true,
|
||||
},
|
||||
SiteConfig: SiteConfig{
|
||||
FirstPage: []string{"index.html", "firstrun.html", "1.html"},
|
||||
|
@ -48,7 +49,8 @@ var (
|
|||
ThreadsPerPage: 20,
|
||||
RepliesOnBoardPage: 3,
|
||||
StickyRepliesOnBoardPage: 1,
|
||||
CyclicalThreadNumPosts: 500,
|
||||
EnableCyclicThreads: true,
|
||||
CyclicThreadNumPosts: 500,
|
||||
BanMessage: "USER WAS BANNED FOR THIS POST",
|
||||
EmbedWidth: 200,
|
||||
EmbedHeight: 164,
|
||||
|
|
|
@ -13,18 +13,18 @@ var (
|
|||
InvalidArgumentErrorStr = "invalid argument(s) passed to event %q"
|
||||
)
|
||||
|
||||
type EventHandler func(string, ...interface{}) error
|
||||
type EventHandler func(string, ...any) error
|
||||
|
||||
// RegisterEvent registers a new event handler to be called when any of the elements of triggers are passed
|
||||
// to TriggerEvent
|
||||
func RegisterEvent(triggers []string, handler func(trigger string, i ...interface{}) error) {
|
||||
func RegisterEvent(triggers []string, handler func(trigger string, i ...any) error) {
|
||||
for _, t := range triggers {
|
||||
registeredEvents[t] = append(registeredEvents[t], handler)
|
||||
}
|
||||
}
|
||||
|
||||
// TriggerEvent triggers the event handler registered to trigger
|
||||
func TriggerEvent(trigger string, data ...interface{}) (handled bool, err error, recovered bool) {
|
||||
func TriggerEvent(trigger string, data ...any) (handled bool, err error, recovered bool) {
|
||||
errEv := gcutil.LogError(nil).Caller(1)
|
||||
defer func() {
|
||||
if a := recover(); a != nil {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
)
|
||||
|
||||
func TestPanicRecover(t *testing.T) {
|
||||
RegisterEvent([]string{"TestPanicRecoverEvt"}, func(tr string, i ...interface{}) error {
|
||||
RegisterEvent([]string{"TestPanicRecoverEvt"}, func(_ string, i ...any) error {
|
||||
t.Log("Testing panic recover")
|
||||
t.Log(i[0])
|
||||
return nil
|
||||
|
@ -21,7 +21,7 @@ func TestPanicRecover(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestEventEditValue(t *testing.T) {
|
||||
RegisterEvent([]string{"TestEventEditValue"}, func(tr string, i ...interface{}) error {
|
||||
RegisterEvent([]string{"TestEventEditValue"}, func(_ string, i ...any) error {
|
||||
p := i[0].(*int)
|
||||
*p += 1
|
||||
return nil
|
||||
|
@ -35,7 +35,7 @@ func TestEventEditValue(t *testing.T) {
|
|||
|
||||
func TestMultipleEventTriggers(t *testing.T) {
|
||||
triggered := map[string]bool{}
|
||||
RegisterEvent([]string{"a", "b"}, func(tr string, i ...interface{}) error {
|
||||
RegisterEvent([]string{"a", "b"}, func(tr string, _ ...any) error {
|
||||
triggered[tr] = true
|
||||
return nil
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func luaEventRegisterHandlerAdapter(l *lua.LState, fn *lua.LFunction) EventHandler {
|
||||
return func(trigger string, data ...interface{}) error {
|
||||
return func(trigger string, data ...any) error {
|
||||
args := []lua.LValue{
|
||||
luar.New(l, trigger),
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ func PreloadModule(l *lua.LState) int {
|
|||
"register_event": func(l *lua.LState) int {
|
||||
table := l.CheckTable(-2)
|
||||
var triggers []string
|
||||
table.ForEach(func(i, val lua.LValue) {
|
||||
table.ForEach(func(_, val lua.LValue) {
|
||||
triggers = append(triggers, val.String())
|
||||
})
|
||||
fn := l.CheckFunction(-1)
|
||||
|
@ -45,7 +45,7 @@ func PreloadModule(l *lua.LState) int {
|
|||
"trigger_event": func(l *lua.LState) int {
|
||||
trigger := l.CheckString(1)
|
||||
numArgs := l.GetTop()
|
||||
var data []interface{}
|
||||
var data []any
|
||||
for i := 2; i <= numArgs; i++ {
|
||||
v := l.CheckAny(i)
|
||||
data = append(data, luautil.LValueToInterface(l, v))
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue