Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ jobs:
run: npm ci

- name: Validate entrypoint loads cleanly
env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
run: |
# node --check ensures syntax correctness
node --check index.js
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ permissions:
contents: read

env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
AFFILIATE_SOURCE: GambleCodez
ADMIN_IDS: "6668510825"
DISCORD_CODE_GENERATION_IMAGE_URL: "https://github.com/gamblecodezcom/Runewager/blob/38c1bbab2d954238829852981514ce92c49d8b10/images/discord_code_generation.png"
Expand Down Expand Up @@ -84,7 +85,8 @@ By: \`${{ github.actor }}\`"
name: Deploy to VPS
runs-on: ubuntu-latest
needs: quality-gates
environment: production
environment:
name: production

steps:
- uses: actions/checkout@v4
Expand Down
42 changes: 36 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,16 @@ function isValidHttpUrl(value) {
}
}

/**
* Escape user-supplied text before interpolating into a Telegram Markdown
* (legacy) message to prevent admin-provided titles or names from breaking
* message formatting.
* Escapes the characters Telegram Markdown treats as special: _ * ` [ ]
*/
function escapeMarkdownV2(text) {
return String(text).replace(/[_*`[\]]/g, '\\$&');
}

function ensureDataDir() {
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
}
Expand Down Expand Up @@ -1698,6 +1708,9 @@ bot.start(safeStepHandler('start', async (ctx) => {
const giveaway = giveawayStore.running.get(gwId);
if (!giveaway) {
await ctx.reply('⚠️ That giveaway has already ended or does not exist.');
} else if (giveaway.joinSurface === 'group') {
// Group-only giveaway — DM joins are not allowed
await ctx.reply('⚠️ This giveaway can only be joined from the group chat.');
} else if (giveaway.participants.has(user.id)) {
await ctx.reply(`✅ You are already entered in Giveaway #${gwId}!`);
} else {
Expand Down Expand Up @@ -3961,9 +3974,6 @@ bot.action('menu_admin_tab', async (ctx) => {
);
});

// admin_dashboard_action: second registration — sends the rich dashboard (replaces the minimal version above)
bot.action('admin_dashboard_action_v2_noop', () => {}); // placeholder to document merge

bot.action('admin_auth_bypass', async (ctx) => {
const user = getUser(ctx);
if (!ADMIN_IDS.includes(Number(user.id))) { await ctx.answerCbQuery('Admin only.'); return; }
Expand Down Expand Up @@ -4279,6 +4289,20 @@ bot.action(/^gw_join_(\d+)$/, async (ctx) => {
return;
}

// Enforce joinSurface: DM-only giveaways can't be joined from a group button
const isPrivate = ctx.chat && ctx.chat.type === 'private';
if (giveaway.joinSurface === 'dm' && !isPrivate) {
await ctx.answerCbQuery('Join via DM only');
await ctx.reply('⚠️ This giveaway can only be joined via the bot DM. Use the DM join link instead.');
return;
}
// Group-only giveaways can't be joined from a DM button
if (giveaway.joinSurface === 'group' && isPrivate) {
await ctx.answerCbQuery('Join from group only');
await ctx.reply('⚠️ This giveaway can only be joined from the group chat.');
return;
}

const checks = evaluateEligibility(user, giveaway);
if (!checks.ok) {
await ctx.answerCbQuery();
Expand Down Expand Up @@ -4403,11 +4427,12 @@ bot.action(/^gw_auto_extend_(\d+)$/, async (ctx) => {
const gwId = Number(ctx.match[1]);
const giveaway = giveawayStore.running.get(gwId);
if (!giveaway) { await ctx.reply(`Giveaway #${gwId} is no longer running.`); return; }
giveaway.awaitingAdminDecision = false; // allow finalizeGiveaway to re-evaluate on next timer fire
giveaway.endTime += 15 * 60 * 1000;
giveaway.extendedCount = (giveaway.extendedCount || 0) + 1;
resetGiveawayTimer(giveaway);
await ctx.reply(`✅ Giveaway #${gwId} extended by 15 minutes (extension ${giveaway.extendedCount}/2).`);
await bot.telegram.sendMessage(giveaway.chatId, `⏱ Giveaway #${gwId} extended! ${15} minutes added — not enough participants yet.`).catch(() => {});
await bot.telegram.sendMessage(giveaway.chatId, `⏱ Giveaway #${gwId} extended! 15 minutes added — not enough participants yet.`).catch(() => {});
});

bot.action(/^gw_force_end_(\d+)$/, async (ctx) => {
Expand Down Expand Up @@ -5397,7 +5422,7 @@ function createGiveaway(config) {
}

async function announceGiveaway(giveaway, botUsername) {
const titleLine = giveaway.title ? `\n📌 *${giveaway.title}*\n` : '';
const titleLine = giveaway.title ? `\n📌 *${escapeMarkdownV2(giveaway.title)}*\n` : '';
const minPartLine = giveaway.minParticipants > 0
? `• Min participants: ${giveaway.minParticipants}\n`
: '';
Expand Down Expand Up @@ -5536,6 +5561,9 @@ async function finalizeGiveaway(gwId, forceEnd = false) {
if (!forceEnd && giveaway.minParticipants > 0 && eligibleCount < giveaway.minParticipants) {
const extendedCount = giveaway.extendedCount || 0;
if (extendedCount < 2) {
// Guard against double-trigger: only notify admins once per timer fire
if (giveaway.awaitingAdminDecision) return;
giveaway.awaitingAdminDecision = true;
// Notify admins with extend/force-end options
for (const adminId of ADMIN_IDS) {
bot.telegram.sendMessage(
Expand All @@ -5558,14 +5586,16 @@ async function finalizeGiveaway(gwId, forceEnd = false) {
}

giveaway.status = 'ended';
giveaway.awaitingAdminDecision = false; // clear the guard
giveawayStore.running.delete(gwId);
giveawayStore.history.set(gwId, giveaway);

giveaway.reminders.forEach((t) => clearTimeout(t));

// Unpin the announcement if we pinned it
// Unpin the announcement if we pinned it, then clear the stored ID
if (giveaway.pinnedMsgId) {
bot.telegram.unpinChatMessage(giveaway.chatId, giveaway.pinnedMsgId).catch(() => {});
giveaway.pinnedMsgId = null;
}

if (eligibleCount === 0) {
Expand Down
Loading