mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-10 01:06:17 +00:00
feat: Simplify update process by removing deprecated methods and enhancing README; improve type safety in tests
This commit is contained in:
@@ -33,21 +33,15 @@ npm start
|
|||||||
**Automatic update script** that keeps your bot up-to-date with the latest version.
|
**Automatic update script** that keeps your bot up-to-date with the latest version.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Two update methods: Git-based or GitHub API (no Git needed)
|
- Uses GitHub API (downloads ZIP - no Git required)
|
||||||
- Preserves your configuration and accounts
|
- Preserves your configuration and accounts
|
||||||
- No merge conflicts with GitHub API method
|
- No merge conflicts, always clean
|
||||||
- Automatic dependency installation and rebuild
|
- Automatic dependency installation and rebuild
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
```bash
|
```bash
|
||||||
# Auto-detect method from config.jsonc
|
# Run update manually
|
||||||
node setup/update/update.mjs
|
node setup/update/update.mjs
|
||||||
|
|
||||||
# Force GitHub API method (recommended)
|
|
||||||
node setup/update/update.mjs --no-git
|
|
||||||
|
|
||||||
# Force Git method
|
|
||||||
node setup/update/update.mjs --git
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Automatic updates:** The bot checks for updates on startup (controlled by `update.enabled` in config.jsonc).
|
**Automatic updates:** The bot checks for updates on startup (controlled by `update.enabled` in config.jsonc).
|
||||||
|
|||||||
@@ -1007,12 +1007,6 @@ export class AccountCreator {
|
|||||||
if (!suggestionsContainer) {
|
if (!suggestionsContainer) {
|
||||||
log(false, 'CREATOR', 'No suggestions found from Microsoft', 'warn', 'yellow')
|
log(false, 'CREATOR', 'No suggestions found from Microsoft', 'warn', 'yellow')
|
||||||
|
|
||||||
// Debug: check HTML content
|
|
||||||
const pageContent = await this.page.content()
|
|
||||||
const hasDataTestId = pageContent.includes('data-testid="suggestions"')
|
|
||||||
const hasToolbar = pageContent.includes('role="toolbar"')
|
|
||||||
log(false, 'CREATOR', `Debug - suggestions in HTML: ${hasDataTestId}, toolbar: ${hasToolbar}`, 'warn', 'yellow')
|
|
||||||
|
|
||||||
// CRITICAL FIX: Generate a new email automatically instead of freezing
|
// CRITICAL FIX: Generate a new email automatically instead of freezing
|
||||||
log(false, 'CREATOR', '🔄 Generating a new email automatically...', 'log', 'cyan')
|
log(false, 'CREATOR', '🔄 Generating a new email automatically...', 'log', 'cyan')
|
||||||
|
|
||||||
|
|||||||
28
src/index.ts
28
src/index.ts
@@ -232,14 +232,6 @@ export class MicrosoftRewardsBot {
|
|||||||
|
|
||||||
log('main', 'BANNER', `Microsoft Rewards Bot v${version}`)
|
log('main', 'BANNER', `Microsoft Rewards Bot v${version}`)
|
||||||
log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`)
|
log('main', 'BANNER', `PID: ${process.pid} | Workers: ${this.config.clusters}`)
|
||||||
|
|
||||||
const upd = this.config.update || {}
|
|
||||||
const updTargets: string[] = []
|
|
||||||
if (upd.method && upd.method !== 'zip') updTargets.push(`Update: ${upd.method}`)
|
|
||||||
if (upd.docker) updTargets.push('Docker')
|
|
||||||
if (updTargets.length > 0) {
|
|
||||||
log('main', 'BANNER', `Auto-Update: ${updTargets.join(', ')}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVersion(): string {
|
private getVersion(): string {
|
||||||
@@ -770,28 +762,12 @@ export class MicrosoftRewardsBot {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const args: string[] = []
|
// New update.mjs uses GitHub API only and takes no CLI arguments
|
||||||
|
|
||||||
// Determine update method from config (github-api is default and recommended)
|
|
||||||
const method = upd.method || 'github-api'
|
|
||||||
|
|
||||||
if (method === 'github-api' || method === 'api' || method === 'zip') {
|
|
||||||
// Use GitHub API method (no Git needed, no conflicts)
|
|
||||||
args.push('--no-git')
|
|
||||||
} else {
|
|
||||||
// Unknown method, default to github-api
|
|
||||||
log('main', 'UPDATE', `Unknown update method "${method}", using github-api`, 'warn')
|
|
||||||
args.push('--no-git')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Docker flag if enabled
|
|
||||||
if (upd.docker) args.push('--docker')
|
|
||||||
|
|
||||||
log('main', 'UPDATE', `Running update script: ${scriptRel}`, 'log')
|
log('main', 'UPDATE', `Running update script: ${scriptRel}`, 'log')
|
||||||
|
|
||||||
// Run update script as a child process and capture exit code
|
// Run update script as a child process and capture exit code
|
||||||
return new Promise<number>((resolve) => {
|
return new Promise<number>((resolve) => {
|
||||||
const child = spawn(process.execPath, [scriptAbs, ...args], { stdio: 'inherit' })
|
const child = spawn(process.execPath, [scriptAbs], { stdio: 'inherit' })
|
||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
log('main', 'UPDATE', `Update script exited with code ${code ?? 0}`, code === 0 ? 'log' : 'warn')
|
log('main', 'UPDATE', `Update script exited with code ${code ?? 0}`, code === 0 ? 'log' : 'warn')
|
||||||
resolve(code ?? 0)
|
resolve(code ?? 0)
|
||||||
|
|||||||
@@ -83,11 +83,10 @@ export interface ConfigProxy {
|
|||||||
|
|
||||||
export interface ConfigUpdate {
|
export interface ConfigUpdate {
|
||||||
enabled?: boolean; // Master toggle for auto-updates (default: true)
|
enabled?: boolean; // Master toggle for auto-updates (default: true)
|
||||||
method?: 'github-api' | 'api' | 'zip'; // Update method (default: "github-api")
|
|
||||||
docker?: boolean; // if true, run docker update routine (compose pull/up) after completion
|
|
||||||
scriptPath?: string; // optional custom path to update script relative to repo root
|
scriptPath?: string; // optional custom path to update script relative to repo root
|
||||||
autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings)
|
autoUpdateConfig?: boolean; // if true, allow auto-update of config.jsonc when remote changes it (default: false to preserve user settings)
|
||||||
autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials)
|
autoUpdateAccounts?: boolean; // if true, allow auto-update of accounts.json when remote changes it (default: false to preserve credentials)
|
||||||
|
// DEPRECATED (removed in v2.56.2+): method, docker - update.mjs now uses GitHub API only
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConfigVacation {
|
export interface ConfigVacation {
|
||||||
|
|||||||
@@ -70,11 +70,10 @@ function stripJsonComments(input: string): string {
|
|||||||
|
|
||||||
// Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface
|
// Normalize both legacy (flat) and new (nested) config schemas into the flat Config interface
|
||||||
function normalizeConfig(raw: unknown): Config {
|
function normalizeConfig(raw: unknown): Config {
|
||||||
// TYPE SAFETY NOTE: Using `any` here is necessary for backwards compatibility
|
// TYPE SAFETY NOTE: Using `any` here is intentional and justified
|
||||||
// JUSTIFIED USE OF `any`: The config format has evolved from flat → nested structure over time
|
// Reason: Config format evolved from flat → nested structure. This function must support BOTH
|
||||||
// This needs to support BOTH formats for backward compatibility with existing user configs
|
// for backward compatibility. Runtime validation via explicit checks ensures safety.
|
||||||
// Runtime validation happens through explicit property checks and the Config interface return type ensures type safety at function boundary
|
// Return type (Config interface) provides type safety at function boundary.
|
||||||
// Alternative approaches (discriminated unions, conditional types) would require extensive runtime checks making code significantly more complex
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const n = (raw || {}) as any
|
const n = (raw || {}) as any
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ if (typeof cleanupInterval.unref === 'function') {
|
|||||||
cleanupInterval.unref()
|
cleanupInterval.unref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the webhook buffer cleanup interval
|
||||||
|
* Call this during graceful shutdown to prevent memory leaks
|
||||||
|
*/
|
||||||
|
export function stopWebhookCleanup(): void {
|
||||||
|
clearInterval(cleanupInterval)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a webhook buffer for the given URL
|
* Get or create a webhook buffer for the given URL
|
||||||
* Buffers batch log messages to reduce Discord API calls
|
* Buffers batch log messages to reduce Discord API calls
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector'
|
import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector'
|
||||||
|
|
||||||
@@ -7,6 +7,9 @@ import { LoginState, LoginStateDetector } from '../src/util/LoginStateDetector'
|
|||||||
* Tests for LoginStateDetector - login flow state machine
|
* Tests for LoginStateDetector - login flow state machine
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Type helper for mock Page objects in tests
|
||||||
|
type MockPage = Parameters<typeof LoginStateDetector.detectState>[0]
|
||||||
|
|
||||||
test('LoginState enum contains expected states', () => {
|
test('LoginState enum contains expected states', () => {
|
||||||
assert.ok(LoginState.EmailPage, 'Should have EmailPage state')
|
assert.ok(LoginState.EmailPage, 'Should have EmailPage state')
|
||||||
assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state')
|
assert.ok(LoginState.PasswordPage, 'Should have PasswordPage state')
|
||||||
@@ -16,19 +19,19 @@ test('LoginState enum contains expected states', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('detectState returns LoginStateDetection structure', async () => {
|
test('detectState returns LoginStateDetection structure', async () => {
|
||||||
// Mock page object
|
// Mock page object with proper Playwright Page interface
|
||||||
const mockPage = {
|
const mockPage = {
|
||||||
url: () => 'https://rewards.bing.com/',
|
url: () => 'https://rewards.bing.com/',
|
||||||
locator: (selector: string) => ({
|
locator: (_selector: string) => ({
|
||||||
first: () => ({
|
first: () => ({
|
||||||
isVisible: () => Promise.resolve(true),
|
isVisible: () => Promise.resolve(true),
|
||||||
textContent: () => Promise.resolve('Test')
|
textContent: () => Promise.resolve('Test')
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
evaluate: () => Promise.resolve(150)
|
evaluate: () => Promise.resolve(150)
|
||||||
}
|
} as unknown as Parameters<typeof LoginStateDetector.detectState>[0]
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage)
|
||||||
|
|
||||||
assert.ok(detection, 'Should return detection object')
|
assert.ok(detection, 'Should return detection object')
|
||||||
assert.ok(typeof detection.state === 'string', 'Should have state property')
|
assert.ok(typeof detection.state === 'string', 'Should have state property')
|
||||||
@@ -57,7 +60,7 @@ test('detectState identifies LoggedIn state on rewards domain', async () => {
|
|||||||
evaluate: () => Promise.resolve(200)
|
evaluate: () => Promise.resolve(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state')
|
assert.equal(detection.state, LoginState.LoggedIn, 'Should detect LoggedIn state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -85,7 +88,7 @@ test('detectState identifies EmailPage state on login.live.com', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state')
|
assert.equal(detection.state, LoginState.EmailPage, 'Should detect EmailPage state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -112,7 +115,7 @@ test('detectState identifies PasswordPage state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state')
|
assert.equal(detection.state, LoginState.PasswordPage, 'Should detect PasswordPage state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -139,7 +142,7 @@ test('detectState identifies TwoFactorRequired state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state')
|
assert.equal(detection.state, LoginState.TwoFactorRequired, 'Should detect TwoFactorRequired state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -167,7 +170,7 @@ test('detectState identifies PasskeyPrompt state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state')
|
assert.equal(detection.state, LoginState.PasskeyPrompt, 'Should detect PasskeyPrompt state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -181,7 +184,7 @@ test('detectState identifies Blocked state', async () => {
|
|||||||
return {
|
return {
|
||||||
first: () => ({
|
first: () => ({
|
||||||
isVisible: () => Promise.resolve(false),
|
isVisible: () => Promise.resolve(false),
|
||||||
textContent: () => Promise.resolve("We can't sign you in")
|
textContent: () => Promise.resolve('We can\'t sign you in')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +198,7 @@ test('detectState identifies Blocked state', async () => {
|
|||||||
evaluate: () => Promise.resolve(100)
|
evaluate: () => Promise.resolve(100)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state')
|
assert.equal(detection.state, LoginState.Blocked, 'Should detect Blocked state')
|
||||||
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
assert.equal(detection.confidence, 'high', 'Should have high confidence')
|
||||||
@@ -213,7 +216,7 @@ test('detectState returns Unknown for ambiguous pages', async () => {
|
|||||||
evaluate: () => Promise.resolve(50)
|
evaluate: () => Promise.resolve(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
const detection = await LoginStateDetector.detectState(mockPage as any)
|
const detection = await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
|
|
||||||
assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages')
|
assert.equal(detection.state, LoginState.Unknown, 'Should return Unknown for ambiguous pages')
|
||||||
assert.equal(detection.confidence, 'low', 'Should have low confidence')
|
assert.equal(detection.confidence, 'low', 'Should have low confidence')
|
||||||
@@ -231,7 +234,7 @@ test('detectState handles errors gracefully', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await LoginStateDetector.detectState(mockPage as any)
|
await LoginStateDetector.detectState(mockPage as MockPage)
|
||||||
assert.fail('Should throw error')
|
assert.fail('Should throw error')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
assert.ok(e instanceof Error, 'Should throw Error instance')
|
assert.ok(e instanceof Error, 'Should throw Error instance')
|
||||||
|
|||||||
Reference in New Issue
Block a user