diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..7530951
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "telegram-approval-join"]
+ path = telegram-approval-join
+ url = ssh://git@git.zio.sh:2222/astra/telegram-join-approval-bot.git
diff --git a/go.mod b/go.mod
index ccc0e15..946fb1a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module git.zio.sh/astra/telegram-approval-join
+module git.zio.sh/astra/telegram-approval-join-nuzzles
go 1.25.3
diff --git a/handlers/admin.go b/handlers/admin.go
index 982f3e5..46e2a8b 100644
--- a/handlers/admin.go
+++ b/handlers/admin.go
@@ -5,7 +5,7 @@ import (
"log"
"strings"
- utils "git.zio.sh/astra/telegram-approval-join/pkg/utils"
+ utils "git.zio.sh/astra/telegram-approval-join-nuzzles/pkg/utils"
api "github.com/OvyFlash/telegram-bot-api"
)
diff --git a/handlers/callbacks.go b/handlers/callbacks.go
index 9b7faf3..df5b3d6 100644
--- a/handlers/callbacks.go
+++ b/handlers/callbacks.go
@@ -6,7 +6,7 @@ import (
"strings"
"time"
- utils "git.zio.sh/astra/telegram-approval-join/pkg/utils"
+ utils "git.zio.sh/astra/telegram-approval-join-nuzzles/pkg/utils"
api "github.com/OvyFlash/telegram-bot-api"
)
diff --git a/handlers/handlers.go b/handlers/handlers.go
index 0fcf2db..5493ae6 100644
--- a/handlers/handlers.go
+++ b/handlers/handlers.go
@@ -3,7 +3,7 @@ package handlers
import (
"sync"
- config "git.zio.sh/astra/telegram-approval-join/config"
+ config "git.zio.sh/astra/telegram-approval-join-nuzzles/config"
api "github.com/OvyFlash/telegram-bot-api"
)
diff --git a/handlers/join.go b/handlers/join.go
index fc84837..b13710b 100644
--- a/handlers/join.go
+++ b/handlers/join.go
@@ -4,7 +4,7 @@ import (
"fmt"
"log"
- utils "git.zio.sh/astra/telegram-approval-join/pkg/utils"
+ utils "git.zio.sh/astra/telegram-approval-join-nuzzles/pkg/utils"
api "github.com/OvyFlash/telegram-bot-api"
)
diff --git a/main.go b/main.go
index cbed5cf..cca6977 100644
--- a/main.go
+++ b/main.go
@@ -3,8 +3,8 @@ package main
import (
"log"
- "git.zio.sh/astra/telegram-approval-join/config"
- "git.zio.sh/astra/telegram-approval-join/handlers"
+ "git.zio.sh/astra/telegram-approval-join-nuzzles/config"
+ "git.zio.sh/astra/telegram-approval-join-nuzzles/handlers"
api "github.com/OvyFlash/telegram-bot-api"
)
diff --git a/patches/0002-nuzzles.patch b/patches/0002-nuzzles.patch
new file mode 100644
index 0000000..3c579df
--- /dev/null
+++ b/patches/0002-nuzzles.patch
@@ -0,0 +1,169 @@
+--- a/config/config.go
++++ b/config/config.go
+@@ -8,14 +8,15 @@ import (
+ )
+
+ type Config struct {
+- BotToken *string `yaml:"bot_token"`
+- AdminChatId *int64 `yaml:"admin_chat_id"`
+- AdminChatTopicId *int `yaml:"admin_chat_topic_id"`
+- TargetChatId *int64 `yaml:"target_chat_id"`
+- EntryMessage string `yaml:"entry_message"`
+- ApprovalMessage string `yaml:"approval_message"`
+- SendApprovalMessage bool `yaml:"send_approval_message"`
+- DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"`
++ BotToken *string `yaml:"bot_token"`
++ AdminChatId *int64 `yaml:"admin_chat_id"`
++ AdminChatTopicId *int `yaml:"admin_chat_topic_id"`
++ TargetChatId *int64 `yaml:"target_chat_id"`
++ EntryMessage string `yaml:"entry_message"`
++ ApprovalMessage string `yaml:"approval_message"`
++ SendApprovalMessage bool `yaml:"send_approval_message"`
++ DeleteRequestAfterDecision bool `yaml:"delete_request_after_decision"`
++ CannedDeclineResponses []string `yaml:"canned_decline_responses"`
+ }
+
+ func (c *Config) LoadConfig() error {
+@@ -47,6 +48,7 @@ func (c *Config) CreateConfig() error {
+ ApprovalMessage: "",
+ SendApprovalMessage: false,
+ DeleteRequestAfterDecision: false,
++ CannedDeclineResponses: []string{},
+ }
+
+ encoder := yaml.NewEncoder(f)
+
+--- a/handlers/callbacks.go
++++ b/handlers/callbacks.go
+@@ -36,6 +36,7 @@ func (bot *Bot) HandleCallbackQuery(query *api.CallbackQuery) {
+ switch action {
+ case "approve":
+ bot.handleApproveRequest(query, user, userString, adminUserString)
++ bot.DeletePendingUser(args)
+ case "decline":
+ bot.handleDeclineRequest(query, user, userString, adminUserString)
+ case "ban":
+@@ -44,10 +45,19 @@ func (bot *Bot) HandleCallbackQuery(query *api.CallbackQuery) {
+ return
+ case "banc":
+ bot.handleBanRequest(query, user, userString, adminUserString)
++ bot.DeletePendingUser(args)
++ case "cannedrespsel":
++ parts := strings.Split(query.Data, "_")
++ if len(parts) >= 3 {
++ var respIdx int
++ fmt.Sscanf(parts[2], "%d", &respIdx)
++ bot.sendCannedResponse(query, user, respIdx)
++ }
++ bot.DeletePendingUser(args)
++ bot.API.Request(api.NewCallback(query.ID, ""))
++ return
+ }
+
+- bot.DeletePendingUser(args)
+-
+ if bot.Config.DeleteRequestAfterDecision {
+ go bot.scheduleMessageDeletion(query.Message.Chat.ID, query.Message.MessageID, 10*time.Second)
+ }
+@@ -93,14 +103,39 @@ func (bot *Bot) handleDeclineRequest(query *api.CallbackQuery, user *ExtendedCha
+ return
+ }
+
+- utils.EditMessage(bot.API, query.Message.Chat.ID, query.Message.MessageID,
+- fmt.Sprintf(AdminDeclinedMsg,
+- userString, user.From.ID, user.JoinReason, adminUserString,
+- time.Now().Format("2006-01-02 15:04:05"),
+- defaultReason,
+- ),
++ messageText := fmt.Sprintf(AdminDeclinedMsg,
++ userString, user.From.ID, user.JoinReason, adminUserString,
++ time.Now().Format("2006-01-02 15:04:05"),
++ defaultReason,
+ )
+
++ edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID, messageText)
++ edit.ParseMode = api.ModeHTML
++ edit.Entities = query.Message.Entities
++
++ if len(bot.Config.CannedDeclineResponses) > 0 {
++ var rows [][]api.InlineKeyboardButton
++ for i, response := range bot.Config.CannedDeclineResponses {
++ // Clean up the response text for button display
++ snippet := strings.TrimSpace(response)
++ snippet = strings.ReplaceAll(snippet, "\n", " ")
++ // Remove multiple consecutive spaces
++ for strings.Contains(snippet, " ") {
++ snippet = strings.ReplaceAll(snippet, " ", " ")
++ }
++ // Truncate to 30 chars for button text
++ if len(snippet) > 30 {
++ snippet = snippet[:30] + "..."
++ }
++ btn := api.NewInlineKeyboardButtonData(snippet, fmt.Sprintf("cannedrespsel_%d_%d", user.From.ID, i))
++ rows = append(rows, []api.InlineKeyboardButton{btn})
++ }
++ keyboard := api.NewInlineKeyboardMarkup(rows...)
++ edit.ReplyMarkup = &keyboard
++ }
++
++ bot.API.Send(edit)
++
+ bot.API.Request(api.NewCallback(query.ID, "Join request declined."))
+ }
+
+@@ -175,12 +210,14 @@ func (bot *Bot) HandleDeclineReason(update *api.Update) {
+ userID, username, joinReason, declinedBy, declinedAt := utils.GetInfoFromMsg(repliedMsg.Text)
+
+ reason := utils.EscapeHTML(update.Message.Text)
+- if strings.HasPrefix(update.Message.Text, "+") {
+- reason = utils.EscapeHTML(update.Message.Text[1:])
++ if !strings.HasPrefix(update.Message.Text, "/") {
++ reason = utils.EscapeHTML(update.Message.Text)
+ utils.SendMessage(bot.API, userID, 0,
+ fmt.Sprintf("Your join request was declined for the following reason:\n\n%s", reason))
+ }
+
++ reason = strings.TrimPrefix(reason, "/")
++
+ utils.EditMessage(bot.API, update.Message.Chat.ID, repliedMsg.MessageID,
+ fmt.Sprintf(AdminDeclinedMsg, username, userID, joinReason, declinedBy, declinedAt, reason))
+
+@@ -188,6 +225,26 @@ func (bot *Bot) HandleDeclineReason(update *api.Update) {
+ bot.API.Send(api.NewDeleteMessage(update.Message.Chat.ID, update.Message.MessageID))
+ }
+
++// sendCannedResponse sends a canned decline response to the declined user.
++func (bot *Bot) sendCannedResponse(query *api.CallbackQuery, user *ExtendedChatJoinRequest, respIdx int) {
++ if respIdx < 0 || respIdx >= len(bot.Config.CannedDeclineResponses) {
++ return
++ }
++
++ reason := utils.EscapeHTML(bot.Config.CannedDeclineResponses[respIdx])
++ utils.SendMessage(bot.API, user.From.ID, 0,
++ fmt.Sprintf("Your join request was declined for the following reason:\n\n%s", reason))
++
++ // Extract user info from original message and reformat with the canned response
++ userID, username, joinReason, declinedBy, declinedAt := utils.GetInfoFromMsg(query.Message.Text)
++
++ edit := api.NewEditMessageText(query.Message.Chat.ID, query.Message.MessageID,
++ fmt.Sprintf(AdminDeclinedMsg, username, userID, joinReason, declinedBy, declinedAt, reason))
++ edit.ParseMode = api.ModeHTML
++ edit.Entities = query.Message.Entities
++ bot.API.Send(edit)
++}
++
+ // parseCallbackData parses the action and user ID from a callback query's data string.
+ func parseCallbackData(data string) (action string, userID int64, err error) {
+ normalized := strings.Join(strings.Split(data, "_"), " ")
+
+--- a/handlers/handlers.go
++++ b/handlers/handlers.go
+@@ -13,7 +13,7 @@ const (
+ AdminDeclinedMsg = "❌ Join #request declined for %s [%d]\n\nJoin reason: %s\nDeclined by: %s\nDeclined at: %s\nDeclined reason: %s"
+ AdminBannedMsg = "🚫 Join #request banned for %s [%d]\n\nJoin reason: %s\nBanned by: %s\nBanned at: %s\nBanned until: %s"
+ AdminFailedMsg = "⚠️ Join #request failed for %s [%d]\n\nJoin reason: %s\nFailure reason: %s"
+- defaultReason = "(no reason provided, reply to this to set one, prepend with + to also send to user)"
++ defaultReason = "(no reason provided, reply to this message to send one to the user, prepend with / to just set it)"
+ )
+
+ // Types shared by handler files
diff --git a/scripts/sync.sh b/scripts/sync.sh
new file mode 100755
index 0000000..880b481
--- /dev/null
+++ b/scripts/sync.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+# sync.sh — Copy telegram-approval-join submodule into internal/, then apply branding patches.
+# Usage: ./scripts/sync.sh
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+SUBMODULE_DIR="$ROOT_DIR/telegram-approval-join"
+PATCH_FILE="$ROOT_DIR/patches/0001-nuzzles.patch"
+
+# ── 1. Ensure submodule is initialised ────────────────────────────────────────
+echo "→ Updating submodule..."
+
+# Capture the submodule commit before update
+OLD_COMMIT=$(git -C "$SUBMODULE_DIR" rev-parse HEAD 2>/dev/null || echo "")
+
+# Update the submodule
+git -C "$ROOT_DIR" submodule update --init --recursive --remote
+
+# Capture the submodule commit after update
+NEW_COMMIT=$(git -C "$SUBMODULE_DIR" rev-parse HEAD 2>/dev/null || echo "")
+
+# Check if there were any changes
+# if [[ "$OLD_COMMIT" == "$NEW_COMMIT" ]] && [[ -n "$OLD_COMMIT" ]]; then
+# echo " Submodule is already up to date. Nothing to do."
+# exit 0
+# fi
+
+# ── 2. Wipe and re-copy the submodule source ──────────────────────────────────
+echo "→ Copying telegram-approval-join source to root..."
+# Clean up directories that will be replaced
+rm -rf "$ROOT_DIR/cmd" "$ROOT_DIR/internal" "$ROOT_DIR/Dockerfile" "$ROOT_DIR/go.mod" "$ROOT_DIR/go.sum" "$ROOT_DIR/config.yaml.example"
+
+# Copy source files, excluding .git and keeping patches/ and scripts/
+rsync -a --stats --exclude='.git' --exclude='internal/telegram-approval-join' \
+ "$SUBMODULE_DIR/" "$ROOT_DIR/" \
+ --exclude='patches' --exclude='scripts' --exclude='README.md'
+
+# Remove scripts/ from .gitignore so we can track our sync script
+sed -i '/^scripts\/$/d' "$ROOT_DIR/.gitignore"
+
+# ── 3. Rewrite the Go module path inside the copied source ────────────────────
+# Change the module from telegram-approval-join to telegram-approval-join-nuzzles
+echo "→ Rewriting module path in go.mod ..."
+sed -i "s|^module git\.zio\.sh/astra/telegram-approval-join|module git.zio.sh/astra/telegram-approval-join-nuzzles|" "$ROOT_DIR/go.mod"
+
+# Fix all import references in the copied source
+echo "→ Rewriting import paths in .go files ..."
+find "$ROOT_DIR" -name '*.go' -not -path "*/telegram-approval-join/*" | xargs sed -i 's|git\.zio\.sh/astra/telegram-approval-join|git.zio.sh/astra/telegram-approval-join-nuzzles|g'
+
+# ── 4. Apply branding string patch ────────────────────────────────────────────
+if [[ -f "$PATCH_FILE" ]]; then
+ echo "→ Applying branding patch ..."
+ # Check if patch applies cleanly first (dry run)
+ if patch --dry-run -p1 -d "$ROOT_DIR" < "$PATCH_FILE" &>/dev/null; then
+ patch -p1 --no-backup-if-mismatch -d "$ROOT_DIR" < "$PATCH_FILE"
+ echo " Patch applied successfully."
+ else
+ echo ""
+ echo "⚠️ Patch did not apply cleanly — telegram-approval-join may have changed."
+ echo " Run the following to see conflicts:"
+ echo " patch --dry-run -p1 -d $ROOT_DIR < $PATCH_FILE"
+ echo ""
+ echo " Update patches/branding.patch to match the new source, then re-run sync."
+ exit 1
+ fi
+else
+ echo " No patch file found at patches/branding.patch — skipping."
+fi
+
+# ── 5. Verify it builds ───────────────────────────────────────────────────────
+echo "→ Verifying build ..."
+go build ./... 2>&1 && echo " Build OK." || { echo "❌ Build failed."; exit 1; }
+
+# ── 6. Commit changes ─────────────────────────────────────────────────────────
+echo "→ Committing changes..."
+git -C "$ROOT_DIR" add -A
+git -C "$ROOT_DIR" commit -m "Update telegram-approval-join submodule and apply branding patches" || true
+
+echo ""
+echo "✅ Sync complete. Root directory is up to date with telegram-approval-join (patched)."
\ No newline at end of file
diff --git a/telegram-approval-join b/telegram-approval-join
new file mode 160000
index 0000000..4e71c09
--- /dev/null
+++ b/telegram-approval-join
@@ -0,0 +1 @@
+Subproject commit 4e71c09c5a6a3f93bd4f0702b0e6f417b6e9e995