diff --git a/Dockerfile b/Dockerfile index e958379f5..e4c8ed895 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:bullseye +FROM golang:bookworm EXPOSE 8000 @@ -11,7 +11,7 @@ WORKDIR /meguca ENV DEBIAN_FRONTEND=noninteractive RUN echo deb-src \ http://ftp.debian.org/debian/ \ - bullseye main contrib non-free \ + bookworm main contrib non-free \ >> /etc/apt/sources.list RUN apt-get update RUN apt-get install -y \ @@ -21,10 +21,18 @@ RUN apt-get install -y \ libwebp-dev \ libopencv-dev \ libgeoip-dev geoip-database \ - python3 python3-requests \ - git lsb-release wget curl netcat postgresql-client gzip + libsqlite3-dev \ + protobuf-compiler \ + exiftool \ + python3 python3-requests python3-pip pipx \ + git lsb-release wget curl netcat-traditional postgresql-client gzip protoc-gen-go liblz4-dev RUN apt-get dist-upgrade -y +# Install yt-dlp +ENV PIPX_HOME=/opt/pipx +ENV PIPX_BIN_DIR=/usr/local/bin +RUN python3 -m pipx install yt-dlp + # Build a known working version of FFmpeg without thumnailer crashing. # # Will investigate cause and fix either thumbnailer or FFmpeg code at a later @@ -42,12 +50,22 @@ WORKDIR /src/FFmpeg RUN ./configure RUN nice -n 19 make -j $(nproc) RUN make install + +# Install RocksDB 10.2.1 +RUN git clone --branch v10.2.1 --depth 1 https://github.com/facebook/rocksdb.git /src/rocksdb +WORKDIR /src/rocksdb +# Warnings when building on Xeon +RUN make DISABLE_WARNING_AS_ERROR=ON static_lib -j$(nproc) +RUN make install WORKDIR /meguca -# Install Node.js -RUN wget -q -O- https://deb.nodesource.com/setup_16.x | bash - +# Install Node.js (Latest LTS) +RUN wget -q -O- https://deb.nodesource.com/setup_lts.x | bash - RUN apt-get install -y nodejs +# Install Go template tool +RUN go install github.com/valyala/quicktemplate/qtc@latest + # Cache dependency downloads, if possible COPY go.mod . COPY go.sum . @@ -58,6 +76,7 @@ RUN npm install --include=dev # Copy and build meguca COPY . . +RUN go mod tidy COPY docs/config.json . RUN sed -i 's/localhost:5432/postgres:5432/' config.json diff --git a/db/threads.go b/db/threads.go index 04f4103f5..80c427eb0 100644 --- a/db/threads.go +++ b/db/threads.go @@ -237,6 +237,15 @@ func CheckIpPostCount(ip string) (count int, err error) { return } +func GetEstimatedPostCount() (count int64, err error) { + err = sq.Select("reltuples::BIGINT"). + From("pg_class"). + Where("relname = ?", "posts"). + QueryRow(). + Scan(&count) + return +} + func GetLatestGeneral(generalName *string) (board string, id uint64, err error) { query := "%" + *generalName + "%" err = stmtGetLatestGeneral.QueryRow(query).Scan(&board, &id) diff --git a/docker-compose.yml b/docker-compose.yml index 56df88154..5a3153df6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,17 @@ services: - "8000:8000" volumes: - images:/meguca/images +# - ./config-local.json:/meguca/config.json:ro depends_on: - postgres + networks: + - isolated + cap_drop: + - NET_RAW + - NET_ADMIN + security_opt: + - no-new-privileges:true + postgres: build: docker/postgres shm_size: "256MB" @@ -18,6 +27,20 @@ services: - POSTGRES_USER=meguca - POSTGRES_PASSWORD=meguca - POSTGRES_DB=meguca + networks: + - isolated + cap_drop: + - NET_RAW + - NET_ADMIN + - SYS_ADMIN + security_opt: + - no-new-privileges:true + +networks: + isolated: + driver: bridge + internal: false + volumes: images: db: diff --git a/server/init.go b/server/init.go index c11f95e33..a15e917ac 100644 --- a/server/init.go +++ b/server/init.go @@ -3,7 +3,6 @@ package server import ( - "github.com/bakape/meguca/websockets" "os" "strconv" @@ -53,7 +52,6 @@ func Start() (err error) { } mlog.Init(mlog.Console) mlog.ConsoleHandler.SetDisplayColor(config.Server.Debug) - websockets.InitGemini() err = util.Parallel(db.LoadDB, assets.CreateDirs) if err != nil { diff --git a/websockets/llm.go b/websockets/llm.go index ba5503328..b42e88a99 100644 --- a/websockets/llm.go +++ b/websockets/llm.go @@ -25,16 +25,25 @@ const ( var ( geminiClient *genai.Client model *genai.GenerativeModel + geminiInitialized bool ) func InitGemini() { + if geminiInitialized { + return + } + if config.Server.GeminiApiKey == "" { + log.Warn("Gemini API key not configured, skipping initialization") + return + } ctx := context.Background() var err error key := &config.Server.GeminiApiKey geminiClient, err = genai.NewClient(ctx, option.WithAPIKey(*key)) if err != nil { - log.Fatal(err) + log.Error("Failed to initialize Gemini client:", err) + return } modelName := "gemini-2.0-flash" @@ -42,8 +51,16 @@ func InitGemini() { modelName = *config.Server.GeminiModel } model = geminiClient.GenerativeModel(modelName) + modelInfo, err := model.Info(ctx) - log.Info("Client model: ", modelInfo.Name) + if err != nil { + log.Error("Failed to get model info:", err) + return + } + if modelInfo != nil { + log.Info("Client model: ", modelInfo.Name) + } + model.SafetySettings = []*genai.SafetySetting{ { Category: genai.HarmCategoryHarassment, @@ -63,6 +80,8 @@ func InitGemini() { }, } + geminiInitialized = true + log.Info("Gemini client initialized successfully") } func GeminiStreamMessages(systemPrompt *string, claudeState *common.ClaudeState, img *[]byte, ext *string, start func(), token func(string), done func()) (err error) { diff --git a/websockets/post_creation.go b/websockets/post_creation.go index 96b7c9d0e..19fd18e9d 100644 --- a/websockets/post_creation.go +++ b/websockets/post_creation.go @@ -107,13 +107,25 @@ func CreateThread(req ThreadCreationRequest, ip string) ( if err != nil { return } - if count == 0 { - log.Warnf("IP address %s tried to create a thread but has NOT posted before", ip) + + // Allow thread creation on new deployments (when total post count is very low) + // Use a simple database function call to check estimated post count + var totalPosts int64 + totalPosts, err = db.GetEstimatedPostCount() + if err != nil { return } - if count == 1 { - log.Warnf("IP address %s tried to create a thread but has only posted once", ip) - return + + // Skip IP post count check if this is a new deployment with < 10 total posts + if totalPosts >= 10 { + if count == 0 { + log.Warnf("IP address %s tried to create a thread but has NOT posted before", ip) + return + } + if count == 1 { + log.Warnf("IP address %s tried to create a thread but has only posted once", ip) + return + } } // Must ensure image token usage is done atomically, as not to cause diff --git a/websockets/post_updates.go b/websockets/post_updates.go index a1d610f42..cda6a8027 100644 --- a/websockets/post_updates.go +++ b/websockets/post_updates.go @@ -308,6 +308,41 @@ func (c *Client) closePost() (err error) { image = nil } //} + + // Check if Gemini is initialized. If not, only admin can initialize it + if !geminiInitialized { + // Get post auth level from database to check if user is admin + var post common.StandalonePost + post, err = db.GetPost(id) + if err != nil { + claude.Status = common.Error + claude.Response.WriteString("Error checking post permissions") + db.UpdateClaude(cid, claude) + feed.SendClaudeComplete(id, true, &claude.Response) + return + } + + if post.Auth == common.Admin { + // Admin can initialize Gemini + InitGemini() + if !geminiInitialized { + // If initialization failed, set error and return + claude.Status = common.Error + claude.Response.WriteString("Failed to initialize Gemini - check API configuration") + db.UpdateClaude(cid, claude) + feed.SendClaudeComplete(id, true, &claude.Response) + return + } + } else { + // Non-admin user trying to use claude before admin initialized - do nothing + claude.Status = common.Error + claude.Response.WriteString("Gemini not initialized. Admin must use claude command first.") + db.UpdateClaude(cid, claude) + feed.SendClaudeComplete(id, true, &claude.Response) + return + } + } + go GeminiStreamMessages(&DefaultSystemPrompt, claude, image, &ext, func() { claude.Status = common.Generating db.UpdateClaude(cid, claude)