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