agent-messenger 2.28.0 → 2.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -1
- package/dist/package.json +1 -1
- package/dist/src/platforms/discord/client.d.ts +6 -2
- package/dist/src/platforms/discord/client.d.ts.map +1 -1
- package/dist/src/platforms/discord/client.js +10 -3
- package/dist/src/platforms/discord/client.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +24 -2
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +343 -53
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts +1 -0
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +123 -11
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts +4 -1
- package/dist/src/platforms/instagram/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/instagram/credential-manager.js +17 -1
- package/dist/src/platforms/instagram/credential-manager.js.map +1 -1
- package/dist/src/platforms/instagram/types.d.ts +10 -8
- package/dist/src/platforms/instagram/types.d.ts.map +1 -1
- package/dist/src/platforms/instagram/types.js.map +1 -1
- package/dist/src/platforms/teams/app-config.d.ts +36 -0
- package/dist/src/platforms/teams/app-config.d.ts.map +1 -0
- package/dist/src/platforms/teams/app-config.js +69 -0
- package/dist/src/platforms/teams/app-config.js.map +1 -0
- package/dist/src/platforms/teams/client.d.ts +20 -2
- package/dist/src/platforms/teams/client.d.ts.map +1 -1
- package/dist/src/platforms/teams/client.js +311 -7
- package/dist/src/platforms/teams/client.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts +10 -0
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +137 -3
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/commands/file.d.ts +3 -0
- package/dist/src/platforms/teams/commands/file.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/file.js +59 -2
- package/dist/src/platforms/teams/commands/file.js.map +1 -1
- package/dist/src/platforms/teams/commands/message.d.ts +11 -0
- package/dist/src/platforms/teams/commands/message.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/message.js +121 -2
- package/dist/src/platforms/teams/commands/message.js.map +1 -1
- package/dist/src/platforms/teams/credential-manager.d.ts +23 -1
- package/dist/src/platforms/teams/credential-manager.d.ts.map +1 -1
- package/dist/src/platforms/teams/credential-manager.js +46 -2
- package/dist/src/platforms/teams/credential-manager.js.map +1 -1
- package/dist/src/platforms/teams/device-code.d.ts +48 -0
- package/dist/src/platforms/teams/device-code.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-code.js +199 -0
- package/dist/src/platforms/teams/device-code.js.map +1 -0
- package/dist/src/platforms/teams/device-login.d.ts +37 -0
- package/dist/src/platforms/teams/device-login.d.ts.map +1 -0
- package/dist/src/platforms/teams/device-login.js +129 -0
- package/dist/src/platforms/teams/device-login.js.map +1 -0
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +12 -0
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/index.d.ts +7 -3
- package/dist/src/platforms/teams/index.d.ts.map +1 -1
- package/dist/src/platforms/teams/index.js +5 -2
- package/dist/src/platforms/teams/index.js.map +1 -1
- package/dist/src/platforms/teams/listener.d.ts +40 -0
- package/dist/src/platforms/teams/listener.d.ts.map +1 -0
- package/dist/src/platforms/teams/listener.js +286 -0
- package/dist/src/platforms/teams/listener.js.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts +3 -0
- package/dist/src/platforms/teams/realm-discovery.d.ts.map +1 -0
- package/dist/src/platforms/teams/realm-discovery.js +33 -0
- package/dist/src/platforms/teams/realm-discovery.js.map +1 -0
- package/dist/src/platforms/teams/token-extractor.d.ts +3 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +60 -0
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/token-provider.d.ts +25 -0
- package/dist/src/platforms/teams/token-provider.d.ts.map +1 -0
- package/dist/src/platforms/teams/token-provider.js +190 -0
- package/dist/src/platforms/teams/token-provider.js.map +1 -0
- package/dist/src/platforms/teams/trouter.d.ts +38 -0
- package/dist/src/platforms/teams/trouter.d.ts.map +1 -0
- package/dist/src/platforms/teams/trouter.js +266 -0
- package/dist/src/platforms/teams/trouter.js.map +1 -0
- package/dist/src/platforms/teams/types.d.ts +103 -0
- package/dist/src/platforms/teams/types.d.ts.map +1 -1
- package/dist/src/platforms/teams/types.js +30 -0
- package/dist/src/platforms/teams/types.js.map +1 -1
- package/dist/src/platforms/webex/types.d.ts +1 -1
- package/dist/src/tui/adapters/instagram-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/instagram-adapter.js +16 -0
- package/dist/src/tui/adapters/instagram-adapter.js.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.d.ts +3 -0
- package/dist/src/tui/adapters/teams-adapter.d.ts.map +1 -1
- package/dist/src/tui/adapters/teams-adapter.js +21 -0
- package/dist/src/tui/adapters/teams-adapter.js.map +1 -1
- package/dist/src/tui/app.js +3 -3
- package/docs/content/docs/cli/instagram.mdx +1 -0
- package/docs/content/docs/cli/teams.mdx +36 -0
- package/docs/content/docs/sdk/teams.mdx +30 -0
- package/package.json +1 -1
- package/skills/agent-channeltalk/SKILL.md +1 -1
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +1 -1
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-imessage/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +1 -1
- package/skills/agent-instagram/references/authentication.md +11 -0
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +1 -1
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +82 -6
- package/skills/agent-teams/references/authentication.md +31 -1
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-telegrambot/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +1 -1
- package/skills/agent-webexbot/SKILL.md +1 -1
- package/skills/agent-wechatbot/SKILL.md +1 -1
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/platforms/discord/client.test.ts +86 -0
- package/src/platforms/discord/client.ts +10 -3
- package/src/platforms/instagram/client.test.ts +478 -33
- package/src/platforms/instagram/client.ts +449 -68
- package/src/platforms/instagram/commands/auth.test.ts +177 -1
- package/src/platforms/instagram/commands/auth.ts +182 -11
- package/src/platforms/instagram/credential-manager.test.ts +30 -0
- package/src/platforms/instagram/credential-manager.ts +23 -1
- package/src/platforms/instagram/types.ts +11 -8
- package/src/platforms/teams/app-config.ts +92 -0
- package/src/platforms/teams/client.test.ts +335 -1
- package/src/platforms/teams/client.ts +370 -7
- package/src/platforms/teams/commands/auth.test.ts +93 -1
- package/src/platforms/teams/commands/auth.ts +182 -3
- package/src/platforms/teams/commands/file.test.ts +92 -1
- package/src/platforms/teams/commands/file.ts +76 -1
- package/src/platforms/teams/commands/message.test.ts +95 -1
- package/src/platforms/teams/commands/message.ts +145 -3
- package/src/platforms/teams/credential-manager.test.ts +20 -0
- package/src/platforms/teams/credential-manager.ts +68 -3
- package/src/platforms/teams/device-code.test.ts +311 -0
- package/src/platforms/teams/device-code.ts +277 -0
- package/src/platforms/teams/device-login.test.ts +288 -0
- package/src/platforms/teams/device-login.ts +206 -0
- package/src/platforms/teams/ensure-auth.ts +11 -0
- package/src/platforms/teams/index.test.ts +10 -0
- package/src/platforms/teams/index.ts +17 -1
- package/src/platforms/teams/listener.test.ts +243 -0
- package/src/platforms/teams/listener.ts +351 -0
- package/src/platforms/teams/realm-discovery.test.ts +73 -0
- package/src/platforms/teams/realm-discovery.ts +42 -0
- package/src/platforms/teams/token-extractor.ts +69 -0
- package/src/platforms/teams/token-provider.test.ts +227 -0
- package/src/platforms/teams/token-provider.ts +284 -0
- package/src/platforms/teams/trouter.test.ts +174 -0
- package/src/platforms/teams/trouter.ts +323 -0
- package/src/platforms/teams/types.test.ts +17 -0
- package/src/platforms/teams/types.ts +96 -0
- package/src/tui/adapters/instagram-adapter.ts +13 -0
- package/src/tui/adapters/teams-adapter.ts +23 -0
- package/src/tui/app.ts +3 -3
|
@@ -5,17 +5,81 @@ import { InstagramCredentialManager } from './credential-manager.js';
|
|
|
5
5
|
import { InstagramError, extractMediaUrl, extractMessageText, getMessageType, } from './types.js';
|
|
6
6
|
const IG_BASE_URL = 'https://i.instagram.com/api/v1';
|
|
7
7
|
const IG_APP_ID = '567067343352427';
|
|
8
|
-
const IG_VERSION = '
|
|
9
|
-
const IG_VERSION_CODE = '
|
|
10
|
-
|
|
11
|
-
const
|
|
8
|
+
const IG_VERSION = '428.0.0.47.67';
|
|
9
|
+
const IG_VERSION_CODE = '719358530';
|
|
10
|
+
// Bloks client versioning id; required header for Bloks endpoints (2FA / CAA flows).
|
|
11
|
+
const IG_BLOKS_VERSION_ID = '5f56efad68e1edec7801f630b5c122704ec5378adbee6609a448f105f34a9c73';
|
|
12
|
+
const IG_CAPABILITIES = '3brTv10=';
|
|
13
|
+
const IG_LOCALE = 'en_US';
|
|
14
|
+
// Instagram silently holds the socket open (no response) for flagged IPs / fingerprints instead
|
|
15
|
+
// of returning an error, so an unbounded fetch hangs forever. Cap every request and surface a
|
|
16
|
+
// clear, actionable error instead of a frozen process.
|
|
17
|
+
const IG_REQUEST_TIMEOUT_MS = 30_000;
|
|
18
|
+
const ANDROID_VERSION = '14';
|
|
19
|
+
const ANDROID_RELEASE = '34';
|
|
12
20
|
const DEVICE_DPI = '480dpi';
|
|
13
|
-
const DEVICE_RESOLUTION = '
|
|
14
|
-
const DEVICE_MANUFACTURER = '
|
|
15
|
-
const DEVICE_MODEL = '
|
|
16
|
-
const
|
|
21
|
+
const DEVICE_RESOLUTION = '1344x2992';
|
|
22
|
+
const DEVICE_MANUFACTURER = 'Google/google';
|
|
23
|
+
const DEVICE_MODEL = 'Pixel 8 Pro';
|
|
24
|
+
const DEVICE_NAME = 'husky';
|
|
25
|
+
const DEVICE_CHIPSET = 'husky';
|
|
17
26
|
export function generateDeviceString() {
|
|
18
|
-
return `${
|
|
27
|
+
return `${ANDROID_RELEASE}/${ANDROID_VERSION}; ${DEVICE_DPI}; ${DEVICE_RESOLUTION}; ${DEVICE_MANUFACTURER}; ${DEVICE_MODEL}; ${DEVICE_NAME}; ${DEVICE_CHIPSET}; en_US; ${IG_VERSION_CODE}`;
|
|
28
|
+
}
|
|
29
|
+
// Instagram no longer verifies the body HMAC; the current mobile API expects a literal
|
|
30
|
+
// "SIGNATURE." prefix (per subzeroid/instagrapi, subzeroid/aiograpi). Sending a real HMAC +
|
|
31
|
+
// ig_sig_key_version is rejected as malformed on /accounts/login/, surfacing as a misleading
|
|
32
|
+
// "account not found" error. URLSearchParams url-encodes the value exactly once.
|
|
33
|
+
function signBody(payload) {
|
|
34
|
+
return { signed_body: `SIGNATURE.${JSON.stringify(payload)}` };
|
|
35
|
+
}
|
|
36
|
+
// jazoest = "2" + sum of ASCII byte values of the input; an anti-bot checksum expected by login.
|
|
37
|
+
function createJazoest(input) {
|
|
38
|
+
let sum = 0;
|
|
39
|
+
for (let i = 0; i < input.length; i++) {
|
|
40
|
+
sum += input.charCodeAt(i);
|
|
41
|
+
}
|
|
42
|
+
return `2${sum}`;
|
|
43
|
+
}
|
|
44
|
+
function generateToken() {
|
|
45
|
+
return randomBytes(16).toString('hex');
|
|
46
|
+
}
|
|
47
|
+
function isTimeoutAbort(error) {
|
|
48
|
+
// AbortSignal.timeout aborts the connect AND the body stream, surfacing as TimeoutError on the
|
|
49
|
+
// fetch and AbortError when a stalled body read is aborted mid-stream; both mean "IG stalled".
|
|
50
|
+
return error instanceof DOMException && (error.name === 'TimeoutError' || error.name === 'AbortError');
|
|
51
|
+
}
|
|
52
|
+
function stalledError(path) {
|
|
53
|
+
return new InstagramError(`Instagram did not respond within ${IG_REQUEST_TIMEOUT_MS / 1000}s (${path}). The connection stalled, ` +
|
|
54
|
+
'which usually means the IP is flagged (VPN/datacenter/proxy or prior automated attempts) or the ' +
|
|
55
|
+
'device fingerprint was rejected. Try "agent-instagram auth extract" from a browser logged into ' +
|
|
56
|
+
'instagram.com instead of password login.', 'request_stalled');
|
|
57
|
+
}
|
|
58
|
+
// One AbortSignal.timeout covers the whole request lifecycle: connect, headers, AND the body read.
|
|
59
|
+
// Reading the body through readText() keeps a post-header stall mapped to request_stalled instead
|
|
60
|
+
// of leaking out as a generic parse failure.
|
|
61
|
+
async function fetchWithTimeout(url, init, path) {
|
|
62
|
+
const signal = AbortSignal.timeout(IG_REQUEST_TIMEOUT_MS);
|
|
63
|
+
let response;
|
|
64
|
+
try {
|
|
65
|
+
response = await fetch(url, { ...init, signal });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (isTimeoutAbort(error))
|
|
69
|
+
throw stalledError(path);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const readText = async () => {
|
|
73
|
+
try {
|
|
74
|
+
return await response.text();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isTimeoutAbort(error))
|
|
78
|
+
throw stalledError(path);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return { response, readText };
|
|
19
83
|
}
|
|
20
84
|
function buildUserAgent(deviceString) {
|
|
21
85
|
return `Instagram ${IG_VERSION} Android (${deviceString})`;
|
|
@@ -23,6 +87,29 @@ function buildUserAgent(deviceString) {
|
|
|
23
87
|
export function generateAndroidDeviceId() {
|
|
24
88
|
return `android-${randomBytes(8).toString('hex')}`;
|
|
25
89
|
}
|
|
90
|
+
export function generateDevice() {
|
|
91
|
+
return {
|
|
92
|
+
phone_id: randomUUID(),
|
|
93
|
+
uuid: randomUUID(),
|
|
94
|
+
android_device_id: generateAndroidDeviceId(),
|
|
95
|
+
advertising_id: randomUUID(),
|
|
96
|
+
client_session_id: randomUUID(),
|
|
97
|
+
device_string: generateDeviceString(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function parseOneClickLoginLink(input) {
|
|
101
|
+
const trimmed = input.trim();
|
|
102
|
+
let query = trimmed.includes('?') ? trimmed.slice(trimmed.indexOf('?') + 1) : trimmed;
|
|
103
|
+
const hashIdx = query.indexOf('#');
|
|
104
|
+
if (hashIdx !== -1)
|
|
105
|
+
query = query.slice(0, hashIdx);
|
|
106
|
+
const params = new URLSearchParams(query);
|
|
107
|
+
const uid = params.get('uid');
|
|
108
|
+
const token = params.get('token');
|
|
109
|
+
if (uid && token)
|
|
110
|
+
return { uid, token };
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
26
113
|
// Instagram DM timestamps are in microseconds
|
|
27
114
|
function microsecondsToISO(us) {
|
|
28
115
|
return new Date(us / 1000).toISOString();
|
|
@@ -33,12 +120,28 @@ export class InstagramClient {
|
|
|
33
120
|
sessionPath = null;
|
|
34
121
|
userId = null;
|
|
35
122
|
cookies = new Map();
|
|
123
|
+
countryCode = '1';
|
|
36
124
|
constructor(credentialManager) {
|
|
37
125
|
this.credentialManager = credentialManager ?? new InstagramCredentialManager();
|
|
38
126
|
}
|
|
127
|
+
setCountryCode(code) {
|
|
128
|
+
this.countryCode = code;
|
|
129
|
+
}
|
|
39
130
|
async login(credentials, accountId) {
|
|
40
131
|
if (credentials) {
|
|
41
|
-
await this.authenticate(credentials.username, credentials.password);
|
|
132
|
+
const result = await this.authenticate(credentials.username, credentials.password);
|
|
133
|
+
if (!result.userId) {
|
|
134
|
+
if (result.requiresTwoFactor) {
|
|
135
|
+
throw new InstagramError('Two-factor authentication required. Use the CLI (auth login/verify) to complete 2FA.', 'two_factor_required');
|
|
136
|
+
}
|
|
137
|
+
if (result.challengeRequired) {
|
|
138
|
+
throw new InstagramError('Instagram requires a security challenge. Use the CLI (auth login/challenge) to resolve it.', 'challenge_required');
|
|
139
|
+
}
|
|
140
|
+
if (result.oneClickEmailAvailable) {
|
|
141
|
+
throw new InstagramError('Password login was rejected; this account can log in by email. Use the CLI "auth login-email" to complete it.', 'one_click_email_available');
|
|
142
|
+
}
|
|
143
|
+
throw new InstagramError('Login did not complete.', 'login_incomplete');
|
|
144
|
+
}
|
|
42
145
|
return this;
|
|
43
146
|
}
|
|
44
147
|
const account = await this.credentialManager.getAccount(accountId);
|
|
@@ -61,40 +164,27 @@ export class InstagramClient {
|
|
|
61
164
|
this.debugLog = fn;
|
|
62
165
|
}
|
|
63
166
|
async authenticate(username, password) {
|
|
64
|
-
const
|
|
65
|
-
const device = {
|
|
66
|
-
phone_id: randomUUID(),
|
|
67
|
-
uuid: randomUUID(),
|
|
68
|
-
android_device_id: generateAndroidDeviceId(),
|
|
69
|
-
advertising_id: randomUUID(),
|
|
70
|
-
client_session_id: randomUUID(),
|
|
71
|
-
device_string: deviceString,
|
|
72
|
-
};
|
|
167
|
+
const device = await this.resolveDevice();
|
|
73
168
|
this.session = { cookies: '', device };
|
|
74
169
|
const encryptionKey = await this.preLoginFlow();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (encryptionKey) {
|
|
78
|
-
this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`);
|
|
79
|
-
encPassword = this.encryptPassword(password, encryptionKey);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
this.debugLog?.('no encryption key, using plaintext password format');
|
|
83
|
-
encPassword = this.plaintextPassword(password);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
this.debugLog?.(`encryption failed: ${err}, falling back to plaintext`);
|
|
88
|
-
encPassword = this.plaintextPassword(password);
|
|
170
|
+
if (!encryptionKey) {
|
|
171
|
+
throw new InstagramError('Instagram did not return a password encryption key. Login cannot proceed safely.', 'encryption_key_missing');
|
|
89
172
|
}
|
|
173
|
+
this.debugLog?.(`encrypting password with key_id=${encryptionKey.keyId}`);
|
|
174
|
+
const encPassword = this.encryptPassword(password, encryptionKey);
|
|
90
175
|
const { status, data } = await this.request('POST', '/accounts/login/', {
|
|
91
176
|
username,
|
|
92
177
|
enc_password: encPassword,
|
|
93
178
|
guid: device.uuid,
|
|
94
179
|
phone_id: device.phone_id,
|
|
180
|
+
_csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
|
|
95
181
|
device_id: device.android_device_id,
|
|
182
|
+
adid: device.advertising_id,
|
|
183
|
+
google_tokens: '[]',
|
|
96
184
|
login_attempt_count: '0',
|
|
97
|
-
|
|
185
|
+
country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
|
|
186
|
+
jazoest: createJazoest(device.phone_id),
|
|
187
|
+
}, { signed: true });
|
|
98
188
|
this.debugLog?.(`login response status=${status} body=${JSON.stringify(data)}`);
|
|
99
189
|
if (data['two_factor_required'] === true) {
|
|
100
190
|
const twoFactorInfo = data['two_factor_info'];
|
|
@@ -110,6 +200,20 @@ export class InstagramClient {
|
|
|
110
200
|
await this.saveSession();
|
|
111
201
|
return { userId: '', challengeRequired: true, challengePath: challengeApiPath };
|
|
112
202
|
}
|
|
203
|
+
if (this.isFacebookLinkedLogin(data)) {
|
|
204
|
+
throw new InstagramError('This account appears to sign in with Facebook and has no usable Instagram password for CLI login. ' +
|
|
205
|
+
'Most reliable fix: log in to instagram.com in your browser, then run "agent-instagram auth extract". ' +
|
|
206
|
+
'Alternatively, give the account its own password. Most reliably, unlink Facebook first: in the Instagram app go to ' +
|
|
207
|
+
'Menu > Settings and privacy > Accounts Center > Manage accounts > (your Facebook profile) Manage > ' +
|
|
208
|
+
'Move out of this Account Center > Move account > Continue; removing it prompts you to create an Instagram password. ' +
|
|
209
|
+
'If Facebook is already unlinked, set one directly under ' +
|
|
210
|
+
'Menu > Settings and privacy > Accounts Center > Password and security > Change password. ' +
|
|
211
|
+
'Setting a password may enable "auth login", though Instagram can still reject automated logins from a new device.', 'facebook_linked');
|
|
212
|
+
}
|
|
213
|
+
if (this.hasLoginButton(data, 'send_one_click_login_email')) {
|
|
214
|
+
this.session.cookies = this.serializeCookies();
|
|
215
|
+
return { userId: '', oneClickEmailAvailable: true };
|
|
216
|
+
}
|
|
113
217
|
if (status !== 200 || data['status'] !== 'ok') {
|
|
114
218
|
const message = data['message'] ?? 'Login failed';
|
|
115
219
|
throw new InstagramError(message, errorType ?? 'login_failed');
|
|
@@ -118,14 +222,25 @@ export class InstagramClient {
|
|
|
118
222
|
return { userId: this.userId ?? '' };
|
|
119
223
|
}
|
|
120
224
|
async twoFactorLogin(username, code, twoFactorIdentifier) {
|
|
225
|
+
const device = this.session?.device;
|
|
121
226
|
const { status, data } = await this.request('POST', '/accounts/two_factor_login/', {
|
|
122
227
|
username,
|
|
123
228
|
verification_code: code,
|
|
124
229
|
two_factor_identifier: twoFactorIdentifier,
|
|
125
230
|
trust_this_device: '1',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
231
|
+
verification_method: '3',
|
|
232
|
+
_csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
|
|
233
|
+
guid: device?.uuid ?? '',
|
|
234
|
+
phone_id: device?.phone_id ?? '',
|
|
235
|
+
device_id: device?.android_device_id ?? '',
|
|
236
|
+
}, { signed: true });
|
|
237
|
+
if (this.isBloksTwoFactorFallback(status, data)) {
|
|
238
|
+
this.debugLog?.('legacy two_factor_login rejected, retrying via Bloks flow');
|
|
239
|
+
const bloksContext = typeof data['two_step_verification_context'] === 'string'
|
|
240
|
+
? data['two_step_verification_context']
|
|
241
|
+
: twoFactorIdentifier;
|
|
242
|
+
return this.bloksTwoFactorLogin(code, bloksContext);
|
|
243
|
+
}
|
|
129
244
|
if (status !== 200 || data['status'] !== 'ok') {
|
|
130
245
|
const message = data['message'] ?? 'Two-factor authentication failed';
|
|
131
246
|
throw new InstagramError(message, 'two_factor_failed');
|
|
@@ -133,6 +248,127 @@ export class InstagramClient {
|
|
|
133
248
|
await this.finalizeLogin(data);
|
|
134
249
|
return { userId: this.userId ?? '' };
|
|
135
250
|
}
|
|
251
|
+
// The login dialog's "send_one_click_login_email" button triggers /accounts/one_click_login/
|
|
252
|
+
// with auto_send=true (NOT /accounts/send_recovery_flow_email/, which is the generic password
|
|
253
|
+
// reset). It needs the account's numeric user id, resolved via a pre-login /users/lookup/.
|
|
254
|
+
async sendOneClickLoginEmail(username) {
|
|
255
|
+
const device = await this.ensureDeviceSession();
|
|
256
|
+
const uid = await this.lookupUserId(username);
|
|
257
|
+
this.debugLog?.(`one_click_login auto_send for uid=${uid}`);
|
|
258
|
+
const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
|
|
259
|
+
uid,
|
|
260
|
+
source: 'one_click_login_email',
|
|
261
|
+
auto_send: 'true',
|
|
262
|
+
guid: device.uuid,
|
|
263
|
+
device_id: device.android_device_id,
|
|
264
|
+
adid: device.advertising_id,
|
|
265
|
+
});
|
|
266
|
+
this.debugLog?.(`one_click_login response status=${status} body=${JSON.stringify(data)}`);
|
|
267
|
+
if (status !== 200 || data['status'] === 'fail') {
|
|
268
|
+
const message = data['message'] ?? 'Failed to send login email';
|
|
269
|
+
throw new InstagramError(message, 'one_click_email_failed');
|
|
270
|
+
}
|
|
271
|
+
const contactPoint = data['obfuscated_email'] ?? data['email'] ?? data['contact_point'] ?? '';
|
|
272
|
+
return { sent: true, contactPoint };
|
|
273
|
+
}
|
|
274
|
+
async lookupUserId(username) {
|
|
275
|
+
const device = await this.ensureDeviceSession();
|
|
276
|
+
this.debugLog?.(`users/lookup for ${username}`);
|
|
277
|
+
const { status, data } = await this.request('POST', '/users/lookup/', {
|
|
278
|
+
q: username,
|
|
279
|
+
directly_sign_in: 'true',
|
|
280
|
+
_csrftoken: this.cookies.get('csrftoken') ?? generateToken(),
|
|
281
|
+
guid: device.uuid,
|
|
282
|
+
device_id: device.android_device_id,
|
|
283
|
+
country_codes: JSON.stringify([{ country_code: this.countryCode, source: ['default'] }]),
|
|
284
|
+
}, { signed: true });
|
|
285
|
+
this.debugLog?.(`users/lookup response status=${status} body=${JSON.stringify(data)}`);
|
|
286
|
+
const userId = (data['user_id'] ?? data['uid'] ?? data['pk']);
|
|
287
|
+
if (userId == null || String(userId).length === 0) {
|
|
288
|
+
const message = data['message'] ?? `Could not find an Instagram account for "${username}".`;
|
|
289
|
+
throw new InstagramError(message, 'user_lookup_failed');
|
|
290
|
+
}
|
|
291
|
+
return String(userId);
|
|
292
|
+
}
|
|
293
|
+
async oneClickLogin(uid, token, source = 'one_click_login_email') {
|
|
294
|
+
const device = await this.ensureDeviceSession();
|
|
295
|
+
const { status, data } = await this.request('POST', '/accounts/one_click_login/', {
|
|
296
|
+
uid,
|
|
297
|
+
token,
|
|
298
|
+
source,
|
|
299
|
+
device_id: device.android_device_id,
|
|
300
|
+
guid: device.uuid,
|
|
301
|
+
adid: device.advertising_id,
|
|
302
|
+
});
|
|
303
|
+
if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
|
|
304
|
+
const message = data['message'] ?? 'One-click login failed';
|
|
305
|
+
throw new InstagramError(message, 'one_click_login_failed');
|
|
306
|
+
}
|
|
307
|
+
if (!data['logged_in_user']) {
|
|
308
|
+
throw new InstagramError('One-click login did not return a session', 'one_click_login_failed');
|
|
309
|
+
}
|
|
310
|
+
await this.finalizeLogin(data);
|
|
311
|
+
return { userId: this.userId ?? '' };
|
|
312
|
+
}
|
|
313
|
+
async ensureDeviceSession() {
|
|
314
|
+
if (!this.session) {
|
|
315
|
+
this.session = { cookies: '', device: await this.resolveDevice() };
|
|
316
|
+
}
|
|
317
|
+
return this.session.device;
|
|
318
|
+
}
|
|
319
|
+
isFacebookLinkedLogin(data) {
|
|
320
|
+
return this.hasLoginButton(data, 'login_with_facebook');
|
|
321
|
+
}
|
|
322
|
+
hasLoginButton(data, action) {
|
|
323
|
+
const buttons = data['buttons'];
|
|
324
|
+
if (!Array.isArray(buttons))
|
|
325
|
+
return false;
|
|
326
|
+
return buttons.some((button) => button?.['action'] === action);
|
|
327
|
+
}
|
|
328
|
+
isBloksTwoFactorFallback(status, data) {
|
|
329
|
+
if (status === 200 && data['status'] === 'ok')
|
|
330
|
+
return false;
|
|
331
|
+
const message = (data['message'] ?? '').trim().toLowerCase();
|
|
332
|
+
return message === 'invalid parameters' || data['two_step_verification_context'] != null;
|
|
333
|
+
}
|
|
334
|
+
async bloksTwoFactorLogin(code, twoFactorIdentifier) {
|
|
335
|
+
const device = this.session?.device;
|
|
336
|
+
const params = {
|
|
337
|
+
client_input_params: {
|
|
338
|
+
code,
|
|
339
|
+
should_trust_device: 1,
|
|
340
|
+
family_device_id: device?.phone_id ?? '',
|
|
341
|
+
device_id: device?.android_device_id ?? '',
|
|
342
|
+
machine_id: this.session?.mid ?? '',
|
|
343
|
+
},
|
|
344
|
+
server_params: {
|
|
345
|
+
challenge: 'totp',
|
|
346
|
+
two_step_verification_context: twoFactorIdentifier,
|
|
347
|
+
flow_source: 'two_factor_login',
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
const { status, data } = await this.request('POST', '/bloks/async_action/com.bloks.www.two_step_verification.verify_code.async/', { params: JSON.stringify(params), bk_client_context: JSON.stringify({ bloks_version: IG_BLOKS_VERSION_ID }) });
|
|
351
|
+
if (status !== 200 || (data['status'] != null && data['status'] !== 'ok')) {
|
|
352
|
+
const message = data['message'] ?? 'Two-factor authentication failed';
|
|
353
|
+
throw new InstagramError(message, 'two_factor_failed');
|
|
354
|
+
}
|
|
355
|
+
const loggedInUser = data['logged_in_user'];
|
|
356
|
+
if (loggedInUser) {
|
|
357
|
+
await this.finalizeLogin(data);
|
|
358
|
+
return { userId: this.userId ?? '' };
|
|
359
|
+
}
|
|
360
|
+
if (this.session?.authorization) {
|
|
361
|
+
const userId = this.session.user_id ?? this.cookies.get('ds_user_id');
|
|
362
|
+
if (!userId) {
|
|
363
|
+
throw new InstagramError('Two-factor authentication failed', 'two_factor_failed');
|
|
364
|
+
}
|
|
365
|
+
this.userId = userId;
|
|
366
|
+
this.session.user_id = userId;
|
|
367
|
+
await this.saveSession();
|
|
368
|
+
return { userId };
|
|
369
|
+
}
|
|
370
|
+
throw new InstagramError('Two-factor authentication failed', 'two_factor_failed');
|
|
371
|
+
}
|
|
136
372
|
async challengeSendCode(apiPath, method = 'email') {
|
|
137
373
|
this.ensureSession();
|
|
138
374
|
const { data: getData } = await this.request('GET', `${apiPath}?guid=${this.session.device.uuid}&device_id=${this.session.device.android_device_id}`);
|
|
@@ -342,15 +578,19 @@ export class InstagramClient {
|
|
|
342
578
|
async preLoginFlow() {
|
|
343
579
|
const url = `${IG_BASE_URL}/qe/sync/`;
|
|
344
580
|
const headers = this.buildHeaders();
|
|
345
|
-
const response = await
|
|
581
|
+
const { response } = await fetchWithTimeout(url, {
|
|
346
582
|
method: 'POST',
|
|
347
583
|
headers,
|
|
348
|
-
body: new URLSearchParams({
|
|
584
|
+
body: new URLSearchParams(signBody({
|
|
349
585
|
id: this.session.device.uuid,
|
|
350
586
|
experiments: 'ig_android_fci,ig_android_device_detection_info_upload,ig_android_device_verification_fb_signup',
|
|
351
|
-
}).toString(),
|
|
352
|
-
});
|
|
587
|
+
})).toString(),
|
|
588
|
+
}, '/qe/sync/');
|
|
353
589
|
this.extractResponseCookies(response.headers);
|
|
590
|
+
const wwwClaim = response.headers.get('x-ig-set-www-claim');
|
|
591
|
+
if (wwwClaim && this.session) {
|
|
592
|
+
this.session.www_claim = wwwClaim;
|
|
593
|
+
}
|
|
354
594
|
const pubKeyHeader = response.headers.get('ig-set-password-encryption-pub-key');
|
|
355
595
|
const keyIdHeader = response.headers.get('ig-set-password-encryption-key-id');
|
|
356
596
|
if (pubKeyHeader && keyIdHeader) {
|
|
@@ -370,8 +610,14 @@ export class InstagramClient {
|
|
|
370
610
|
const tag = cipher.getAuthTag();
|
|
371
611
|
const pubKeyPem = Buffer.from(key.publicKey, 'base64').toString('utf-8');
|
|
372
612
|
const rsaEncrypted = publicEncrypt({ key: pubKeyPem, padding: constants.RSA_PKCS1_PADDING }, aesKey);
|
|
613
|
+
if (!/^(?:0|[1-9]\d*)$/.test(key.keyId)) {
|
|
614
|
+
throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid');
|
|
615
|
+
}
|
|
616
|
+
const keyIdNum = Number(key.keyId);
|
|
617
|
+
if (keyIdNum > 255) {
|
|
618
|
+
throw new InstagramError(`Invalid password encryption key id: ${key.keyId}`, 'encryption_key_invalid');
|
|
619
|
+
}
|
|
373
620
|
// Wire format: version(1) | keyId(1) | iv(12) | rsaLen(2 LE) | rsaEncrypted | tag(16) | aesEncrypted
|
|
374
|
-
const keyIdNum = Number.parseInt(key.keyId, 10);
|
|
375
621
|
const buf = Buffer.alloc(1 + 1 + 12 + 2 + rsaEncrypted.length + 16 + encrypted.length);
|
|
376
622
|
let offset = 0;
|
|
377
623
|
buf.writeUInt8(1, offset);
|
|
@@ -389,50 +635,84 @@ export class InstagramClient {
|
|
|
389
635
|
encrypted.copy(buf, offset);
|
|
390
636
|
return `#PWD_INSTAGRAM:4:${timestamp}:${buf.toString('base64')}`;
|
|
391
637
|
}
|
|
392
|
-
|
|
393
|
-
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
394
|
-
return `#PWD_INSTAGRAM:0:${timestamp}:${password}`;
|
|
395
|
-
}
|
|
396
|
-
async request(method, path, body) {
|
|
638
|
+
async request(method, path, body, options) {
|
|
397
639
|
const url = `${IG_BASE_URL}${path}`;
|
|
398
640
|
const headers = this.buildHeaders();
|
|
399
|
-
const
|
|
641
|
+
const requestInit = { method, headers };
|
|
400
642
|
if (body) {
|
|
401
|
-
|
|
643
|
+
const payload = options?.signed ? signBody(body) : body;
|
|
644
|
+
requestInit.body = new URLSearchParams(payload).toString();
|
|
402
645
|
}
|
|
403
|
-
const response = await
|
|
646
|
+
const { response, readText } = await fetchWithTimeout(url, requestInit, path);
|
|
404
647
|
this.extractResponseCookies(response.headers);
|
|
405
648
|
const authHeader = response.headers.get('ig-set-authorization');
|
|
406
649
|
if (authHeader && this.session) {
|
|
407
650
|
this.session.authorization = authHeader;
|
|
408
651
|
}
|
|
652
|
+
const wwwClaim = response.headers.get('x-ig-set-www-claim');
|
|
653
|
+
if (wwwClaim && this.session) {
|
|
654
|
+
this.session.www_claim = wwwClaim;
|
|
655
|
+
}
|
|
656
|
+
const rawBody = await readText();
|
|
409
657
|
if (response.status === 429) {
|
|
410
|
-
|
|
658
|
+
this.debugLog?.(`429 from ${path} body=${rawBody}`);
|
|
659
|
+
const igMessage = this.extractJsonMessage(rawBody);
|
|
660
|
+
throw new InstagramError(igMessage
|
|
661
|
+
? `Rate limited by Instagram: ${igMessage} Wait before trying again.`
|
|
662
|
+
: 'Rate limited by Instagram. Try again later.', 'rate_limited');
|
|
411
663
|
}
|
|
412
664
|
let data;
|
|
413
665
|
try {
|
|
414
|
-
data =
|
|
666
|
+
data = JSON.parse(rawBody);
|
|
415
667
|
}
|
|
416
668
|
catch {
|
|
417
669
|
throw new InstagramError(`Failed to parse response from ${path}`, response.status);
|
|
418
670
|
}
|
|
419
671
|
return { status: response.status, data };
|
|
420
672
|
}
|
|
673
|
+
extractJsonMessage(body) {
|
|
674
|
+
try {
|
|
675
|
+
const parsed = JSON.parse(body);
|
|
676
|
+
return parsed['message'] ?? '';
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return '';
|
|
680
|
+
}
|
|
681
|
+
}
|
|
421
682
|
buildHeaders() {
|
|
422
683
|
const deviceString = this.session?.device.device_string ?? generateDeviceString();
|
|
684
|
+
// Instagram's edge silently drops connections whose fingerprint headers don't match a real
|
|
685
|
+
// Android client. This is a partial alignment toward the current app profile
|
|
686
|
+
// (per subzeroid/instagrapi) covering the highest-signal headers, not a full fingerprint match
|
|
687
|
+
// (e.g. X-Tigon-Is-Retry, X-Zero-*, X-IG-Nav-Chain, X-IG-SALT-IDS are intentionally omitted).
|
|
423
688
|
const headers = {
|
|
424
689
|
'User-Agent': buildUserAgent(deviceString),
|
|
690
|
+
'X-IG-App-Locale': IG_LOCALE,
|
|
691
|
+
'X-IG-Device-Locale': IG_LOCALE,
|
|
692
|
+
'X-IG-Mapped-Locale': IG_LOCALE,
|
|
693
|
+
'X-Pigeon-Session-Id': `UFS-${randomUUID()}-0`,
|
|
694
|
+
'X-Pigeon-Rawclienttime': (Date.now() / 1000).toFixed(3),
|
|
695
|
+
'X-IG-Bandwidth-Speed-KBPS': '-1.000',
|
|
696
|
+
'X-IG-Bandwidth-TotalBytes-B': '0',
|
|
697
|
+
'X-IG-Bandwidth-TotalTime-MS': '0',
|
|
698
|
+
'X-IG-App-Startup-Country': 'US',
|
|
425
699
|
'X-IG-App-ID': IG_APP_ID,
|
|
426
|
-
'X-IG-Capabilities':
|
|
700
|
+
'X-IG-Capabilities': IG_CAPABILITIES,
|
|
427
701
|
'X-IG-Connection-Type': 'WIFI',
|
|
428
702
|
'X-IG-Timezone-Offset': String(new Date().getTimezoneOffset() * -60),
|
|
703
|
+
'X-Bloks-Version-Id': IG_BLOKS_VERSION_ID,
|
|
704
|
+
'X-FB-HTTP-Engine': 'Tigon/MNS/TCP',
|
|
705
|
+
Priority: 'u=3',
|
|
706
|
+
'IG-INTENDED-USER-ID': this.userId ?? '0',
|
|
429
707
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
430
708
|
Accept: '*/*',
|
|
431
709
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
432
710
|
};
|
|
711
|
+
headers['X-IG-WWW-Claim'] = this.session?.www_claim ?? '0';
|
|
433
712
|
if (this.session) {
|
|
434
713
|
headers['X-IG-Device-ID'] = this.session.device.uuid;
|
|
435
714
|
headers['X-IG-Android-ID'] = this.session.device.android_device_id;
|
|
715
|
+
headers['X-IG-Family-Device-ID'] = this.session.device.phone_id;
|
|
436
716
|
if (this.session.authorization) {
|
|
437
717
|
headers['Authorization'] = this.session.authorization;
|
|
438
718
|
}
|
|
@@ -499,6 +779,16 @@ export class InstagramClient {
|
|
|
499
779
|
}
|
|
500
780
|
await this.saveSession();
|
|
501
781
|
}
|
|
782
|
+
// Reuse one persisted device per machine so repeat logins present a consistent fingerprint.
|
|
783
|
+
// Regenerating device ids each attempt looks like a brand-new phone and hurts login trust.
|
|
784
|
+
async resolveDevice() {
|
|
785
|
+
const existing = await this.credentialManager.loadDevice();
|
|
786
|
+
if (existing)
|
|
787
|
+
return existing;
|
|
788
|
+
const device = generateDevice();
|
|
789
|
+
await this.credentialManager.saveDevice(device);
|
|
790
|
+
return device;
|
|
791
|
+
}
|
|
502
792
|
async saveSession() {
|
|
503
793
|
if (!this.session || !this.sessionPath)
|
|
504
794
|
return;
|