diff --git a/.gitignore b/.gitignore index f5d924e..8054410 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -debian/changelog -scripts/twit-link +tumble.sqlite +bin/* diff --git a/Makefile b/Makefile index 7fce1a9..f0743ef 100644 --- a/Makefile +++ b/Makefile @@ -1,87 +1,45 @@ - PKGNAME=tumble -TMP_PATTERN:=$(shell mktemp -d -u -p . -t rpmbuild-XXXXXXX) -TMPDIR=$(shell pwd)/$(TMP_PATTERN) -TAR_TMP_DIR:=$(shell mktemp -d -u -t tarball-XXXXXXX) - -DATADIR=$(DESTDIR)/srv/www/$(PKGNAME) -CONFDIR=$(DESTDIR)/etc/ -CRON_DIR=$(CONFDIR)/cron.hourly -SPEC_FILE=$(PKGNAME).spec +VERSION=$(shell git describe --tags --always | sed -e 's/-/\./g') +BINARY_NAME=tumble +BUILD_DIR=bin -RPMBUILD := $(shell if test -f /usr/bin/rpmbuild ; then echo /usr/bin/rpmbuild ; else echo "x" ; fi) -RPM_DEFINES = --define "_specdir $(TMPDIR)/SPECS" --define "_rpmdir $(TMPDIR)/RPMS" --define "_sourcedir $(TMPDIR)/SOURCES" --define "_srcrpmdir $(TMPDIR)/SRPMS" --define "_builddir $(TMPDIR)/BUILD" -MAKE_DIRS= $(TMPDIR)/SPECS $(TMPDIR)/SOURCES $(TMPDIR)/BUILD $(TMPDIR)/SRPMS $(TMPDIR)/RPMS -VERSION=$(shell git describe | sed -e 's/-/\./g') -TARBALL=$(PKGNAME)-$(VERSION).tar.gz +.PHONY: all build clean test deps docs kill restart reset-db load-fixtures -DEBIAN :=$(shell test -f "/etc/debian_version" && echo 'debian' || echo 'x') +all: build -ifeq ($(DEBIAN), debian) -APACHE_DIR=$(CONFDIR)apache2/sites-available/ -else -APACHE_DIR=$(CONFDIR)httpd/conf.d -endif +deps: + go mod download +GIT_COMMIT=$(shell git rev-parse --short HEAD) +LDFLAGS=-ldflags "-X tumble/internal/version.CommitHash=$(GIT_COMMIT)" build: - #go build -o scripts/twit-link scripts/twit-link.go - - -install: - mkdir -p $(DATADIR) $(APACHE_DIR) $(CONFDIR)/$(PKGNAME) - install -p -m644 htdocs/config.yaml $(CONFDIR)/$(PKGNAME) - cp -pr htdocs $(DATADIR) - mkdir -p $(DESTDIR)/usr/local/bin - #go build -o scripts/twit-link scripts/twit-link.go - #cp -pr scripts/twit-link $(DESTDIR)/usr/local/bin + mkdir -p $(BUILD_DIR) + CGO_ENABLED=0 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/tumble -tarball: clean - mkdir -p $(TAR_TMP_DIR)/$(PKGNAME)-$(VERSION) - cd ..; cp -pr $(PKGNAME)/* $(TAR_TMP_DIR)/$(PKGNAME)-$(VERSION) - cd $(TAR_TMP_DIR); tar pczf $(TARBALL) $(PKGNAME)-$(VERSION) - mv $(TAR_TMP_DIR)/$(TARBALL) . - rm -rf $(TAR_TMP_DIR) +clean: + rm -rf $(BUILD_DIR) -uninstall: - rm -rf $(DATADIR) - rm -rf $(APACHE_DIR)/$(PKGNAME).conf +test: + go test -v ./... -clean: - rm -f $(TARBALL) *.rpm - rm -f scripts/twit-link twit-link - rm -rf debian/changelog debian/$(PKGNAME)* debian/tmp debian/files - rm -rf BUILD SRPMS RPMS SPECS SOURCES - rm -rf ./rpmbuild-* ./tarball-* ./$(PKGNAME)*gz +test-api: build + ./tests/api_test.sh +docs: + @echo "Generating API docs..." + # Placeholder for Swagger/OpenAPI generation + # e.g. swag init -g cmd/tumble/main.go --output docs/api -srpm: tarball - @mkdir -p $(MAKE_DIRS) - cp -f $(TARBALL) $(TMPDIR)/SOURCES - cp -f $(SPEC_FILE) $(TMPDIR)/SPECS - sed -i 's/==VERSION==/$(VERSION)/g' $(TMPDIR)/SPECS/$(SPEC_FILE) - @wait - $(RPMBUILD) $(RPM_DEFINES) -bs $(TMPDIR)/SPECS/$(SPEC_FILE) - @mv -f $(TMPDIR)/SRPMS/* . - @rm -rf $(TMPDIR) +kill: + -pkill -f $(BUILD_DIR)/$(BINARY_NAME) -deb: - sed -e 's/==VERSION==/$(VERSION)/g' debian/changelog.in > debian/changelog - @wait - dpkg-buildpackage +restart: kill build + $(BUILD_DIR)/$(BINARY_NAME) conf/config.yaml & -rpm: clean tarball - @mkdir -p $(MAKE_DIRS) - cp -f $(TARBALL) $(TMPDIR)/SOURCES - cp -f $(SPEC_FILE) $(TMPDIR)/SPECS - sed -i 's/==VERSION==/$(VERSION)/g' $(TMPDIR)/SPECS/$(SPEC_FILE) - @wait - $(RPMBUILD) $(RPM_DEFINES) -ba $(TMPDIR)/SPECS/$(SPEC_FILE) - @mv -f $(TMPDIR)/RPMS/noarch/* . - @rm -rf $(TMPDIR) +reset-db: + rm -f tumble.sqlite -tempdir: - echo $(TMPDIR) +load-fixtures: + ./tests/load_fixtures.sh -test: - prove -l t/*.t diff --git a/cmd/tumble/main.go b/cmd/tumble/main.go new file mode 100644 index 0000000..f187df1 --- /dev/null +++ b/cmd/tumble/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + + "tumble/internal/assets" + "tumble/internal/config" + "tumble/internal/data" + "tumble/internal/handler" + "tumble/internal/service" + "tumble/internal/templates" +) + +func main() { + // Load Config + cfgPath := "conf/config.yaml" // Default or flag + if len(os.Args) > 1 { + cfgPath = os.Args[1] + } + + cfg, err := config.Load(cfgPath) + if err != nil { + log.Printf("Warning: Could not load config from %s: %v", cfgPath, err) + // Proceed with defaults or fail? Perl requires config.yaml in htdocs usually. + // We'll assume we need one. + // Try htdocs/config.yaml + cfg, err = config.Load("htdocs/config.yaml") + if err != nil { + log.Fatalf("Fatal: Could not load config: %v", err) + } + } + + // Init DB + store, err := data.NewStore(cfg.Driver, cfg.DSN()) + if err != nil { + log.Fatalf("Fatal: Could not connect to DB: %v", err) + } + defer store.Close() + + // Auto-Migrate + if err := store.Bootstrap(context.TODO()); err != nil { + log.Fatalf("Fatal: Database bootstrap failed: %v", err) + } + + // Init Service + svc := service.NewContentService(cfg) + + // Init Renderer + renderer, err := templates.NewRenderer() + if err != nil { + log.Fatalf("Fatal: Could not init renderer: %v", err) + } + + // Init Handler + h := handler.NewHandler(cfg, store, svc, renderer) + + // Router + mux := http.NewServeMux() + + // Main Routes + mux.HandleFunc("/", h.Index) + mux.HandleFunc("/index.cgi", h.Index) + mux.HandleFunc("/search.cgi", h.Search) + mux.HandleFunc("/irclink/", h.IRCLinkHandler) // Handles /irclink/?id and posts + mux.HandleFunc("/ogpreview.cgi", h.OGPreviewHandler) + + // v0 Routes (Aliased) + mux.HandleFunc("/v0/", h.Index) + mux.HandleFunc("/v0/index.cgi", h.Index) + mux.HandleFunc("/v0/search.cgi", h.Search) + mux.HandleFunc("/v0/irclink/", h.IRCLinkHandler) + mux.HandleFunc("/v0/ogpreview.cgi", h.OGPreviewHandler) + mux.HandleFunc("/v0/quote/", h.QuoteHandler) + + // Quote Handler (Legacy) + mux.HandleFunc("/quote/", h.QuoteHandler) + mux.HandleFunc("/quote/index.cgi", h.QuoteHandler) + + // Static Assets + // Serve from embedded FS + // "/css/" -> internal/assets/css + fileServer := http.FileServer(http.FS(assets.StaticFS)) + mux.Handle("/css/", fileServer) + mux.Handle("/img/", fileServer) + mux.Handle("/buttons/", fileServer) + mux.Handle("/favicon.ico", fileServer) + + // Start + addr := ":8080" // Default or from config? Perl was CGI so port wasn't in config. + log.Printf("Starting tumble server on %s", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("Server failed: %v", err) + } +} diff --git a/conf/config.yaml b/conf/config.yaml new file mode 100644 index 0000000..f4cd021 --- /dev/null +++ b/conf/config.yaml @@ -0,0 +1,3 @@ +driver: sqlite +database: tumble.sqlite +baseurl: localhost:8080 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbd6afd --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module tumble + +go 1.25.5 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.33 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sys v0.36.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.43.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3628979 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA= +modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= diff --git a/internal/assets/buttons/button.cgi b/internal/assets/buttons/button.cgi new file mode 100755 index 0000000..3e2d16e --- /dev/null +++ b/internal/assets/buttons/button.cgi @@ -0,0 +1,82 @@ +#!/usr/bin/perl -w + +use CGI; + +use YAML qw( LoadFile ); + +use strict; + +my $cgi = new CGI; + +my $user = $cgi->param( 'user' ); + +my $config = LoadFile( '../config.yaml' ); +my $url = $config->{'baseurl'}; + +if ( $user ) { + print "Content-type: text/html\n\n"; + + print qq( + + + + tumblefish buttons + + + + + +
+
+ tumblefish. +
+ +
+
+
!!
+
buttons
+
yay!
+
+
+
+ So how do I install this crap?? +
+
+