eslint: upgrade to version 9 + tests: remove tests

This commit is contained in:
Rajeh Taher
2025-07-18 22:28:39 +03:00
parent 241fe3e691
commit 90781ce0f2
22 changed files with 10790 additions and 9089 deletions
-10
View File
@@ -1,10 +0,0 @@
# Ignore artifacts:
lib
coverage
*.lock
.eslintrc.json
src/WABinary/index.ts
WAProto
Example/Example.ts
docs
proto-extract
-39
View File
@@ -1,39 +0,0 @@
{
"extends": ["@whiskeysockets", "plugin:prettier/recommended"],
"plugins": ["prettier"],
"parserOptions": {
"sourceType": "module",
"project": "./tsconfig.json"
},
"ignorePatterns": ["src/Tests/*"],
"rules": {
"camelcase": "off",
"indent": "off",
"@typescript-eslint/no-explicit-any": [
"warn",
{
"ignoreRestArgs": true
}
],
"@typescript-eslint/no-inferrable-types": ["warn"],
"@typescript-eslint/no-redundant-type-constituents": ["warn"],
"@typescript-eslint/no-unnecessary-type-assertion": ["warn"],
"no-restricted-syntax": "off",
"keyword-spacing": "off",
"implicit-arrow-linebreak": ["off"],
"space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
],
"@typescript-eslint/no-unused-vars": [
"error",
{
"caughtErrors": "none"
}
]
}
}
+1
View File
@@ -0,0 +1 @@
nodeLinker: node-modules
+72
View File
@@ -0,0 +1,72 @@
import { defineConfig, globalIgnores } from "eslint/config";
import prettier from "eslint-plugin-prettier";
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import base from "@whiskeysockets/eslint-config";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all
});
export default defineConfig([globalIgnores([
"src/Tests/*",
"**/lib",
"**/coverage",
"**/*.lock",
"**/.eslintrc.json",
"src/WABinary/index.ts",
"**/WAProto",
"Example/Example.ts",
"**/docs",
"**/proto-extract",
]),
...base,
{
extends: compat.extends("plugin:prettier/recommended"),
plugins: {
prettier,
},
languageOptions: {
ecmaVersion: 5,
sourceType: "module",
parserOptions: {
project: "./tsconfig.json",
},
},
rules: {
camelcase: "off",
indent: "off",
"@typescript-eslint/no-explicit-any": ["warn", {
ignoreRestArgs: true,
}],
"@typescript-eslint/no-inferrable-types": ["warn"],
"@typescript-eslint/no-redundant-type-constituents": ["warn"],
"@typescript-eslint/no-unnecessary-type-assertion": ["warn"],
"no-restricted-syntax": "off",
"keyword-spacing": "off",
"implicit-arrow-linebreak": ["off"],
"space-before-function-paren": ["error", {
anonymous: "always",
named: "never",
asyncArrow: "always",
}],
"@typescript-eslint/no-unused-vars": ["error", {
caughtErrors: "none",
}],
},
}]);
+7 -8
View File
@@ -43,13 +43,15 @@
"@hapi/boom": "^9.1.3",
"async-mutex": "^0.5.0",
"axios": "^1.6.0",
"libsignal": "github:WhiskeySockets/libsignal-node",
"libsignal": "git+https://github.com/whiskeysockets/libsignal-node",
"music-metadata": "^11.7.0",
"pino": "^9.6",
"protobufjs": "^7.2.4",
"ws": "^8.13.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.31.0",
"@types/jest": "^30.0.0",
"@types/node": "^16.0.0",
"@types/ws": "^8.0.0",
@@ -57,18 +59,18 @@
"@typescript-eslint/parser": "^8.32.0",
"@whiskeysockets/eslint-config": "github:whiskeysockets/eslint-config",
"conventional-changelog-cli": "^2.2.2",
"eslint": "^8.0.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.4.0",
"jest": "^29.7.0",
"jimp": "^1.6.0",
"jiti": "^2.4.2",
"json": "^11.0.0",
"link-preview-js": "^3.0.0",
"open": "^8.4.2",
"prettier": "^3.5.3",
"protobufjs-cli": "^1.1.3",
"release-it": "^15.10.3",
"sharp": "^0.34.2",
"ts-jest": "^29.3.2",
"tsx": "^4.20.3",
"typedoc": "^0.27.9",
@@ -79,7 +81,7 @@
"audio-decode": "^2.1.3",
"jimp": "^1.6.0",
"link-preview-js": "^3.0.0",
"sharp": "^0.34.2"
"sharp": "*"
},
"peerDependenciesMeta": {
"audio-decode": {
@@ -90,12 +92,9 @@
},
"link-preview-js": {
"optional": true
},
"sharp": {
"optional": true
}
},
"packageManager": "yarn@1.22.19",
"packageManager": "yarn@4.9.2",
"engines": {
"node": ">=20.0.0"
}
+1 -1
View File
@@ -9,7 +9,7 @@ const _gcLimit = 10000
async function _asyncQueueExecutor(queue: Array<QueueJob<any>>, cleanup: () => void): Promise<void> {
let offt = 0
// eslint-disable-next-line no-constant-condition
while (true) {
const limit = Math.min(queue.length, _gcLimit)
for (let i = offt; i < limit; i++) {
+10 -10
View File
@@ -1095,9 +1095,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// Handles newsletter notifications
async function handleNewsletterNotification(node: BinaryNode) {
const from = node.attrs.from
const [child] = getAllBinaryNodeChildren(node)
const author = node.attrs.participant
const from = node.attrs.from!
const child = getAllBinaryNodeChildren(node)[0]!
const author = node.attrs.participant!
logger.info({ from, child }, 'got newsletter notification')
@@ -1105,7 +1105,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'reaction':
const reactionUpdate = {
id: from,
server_id: child.attrs.message_id,
server_id: child.attrs.message_id!,
reaction: {
code: getBinaryNodeChildString(child, 'reaction'),
count: 1
@@ -1117,7 +1117,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case 'view':
const viewUpdate = {
id: from,
server_id: child.attrs.message_id,
server_id: child.attrs.message_id!,
count: parseInt(child.content?.toString() || '0', 10)
}
ev.emit('newsletter.view', viewUpdate)
@@ -1127,9 +1127,9 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const participantUpdate = {
id: from,
author,
user: child.attrs.jid,
action: child.attrs.action,
new_role: child.attrs.role
user: child.attrs.jid!,
action: child.attrs.action!,
new_role: child.attrs.role!
}
ev.emit('newsletter-participants.update', participantUpdate)
break
@@ -1168,7 +1168,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
fromMe: false
},
message: messageProto,
messageTimestamp: +child.attrs.t
messageTimestamp: +child.attrs.t!
})
await upsertMessage(fullMessage, 'append')
logger.info('Processed plaintext newsletter message')
@@ -1229,7 +1229,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
if (update.jid && update.user) {
ev.emit('newsletter-participants.update', {
id: update.jid,
author: node.attrs.from,
author: node.attrs.from!,
user: update.user,
new_role: 'ADMIN',
action: 'promote'
+3 -2
View File
@@ -47,7 +47,8 @@ import {
} from '../WABinary'
import { USyncQuery, USyncUser } from '../WAUSync'
import { makeGroupsSocket } from './groups'
import { makeNewsletterSocket, NewsletterSocket } from './newsletter'
import type { NewsletterSocket } from './newsletter'
import { makeNewsletterSocket } from './newsletter'
export const makeMessagesSocket = (config: SocketConfig) => {
const {
@@ -307,7 +308,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
const msgId = await relayMessage(meJid, protocolMessage, {
additionalAttributes: {
category: 'peer',
// eslint-disable-next-line camelcase
push_priority: 'high_force'
}
})
+1 -1
View File
@@ -1,5 +1,5 @@
import { Boom } from '@hapi/boom'
import { BinaryNode } from '../WABinary'
import type { BinaryNode } from '../WABinary'
import { getBinaryNodeChild, S_WHATSAPP_NET } from '../WABinary'
const wMexQuery = (
+3 -2
View File
@@ -1,8 +1,9 @@
import type { NewsletterCreateResponse, WAMediaUpload } from '../Types'
import { NewsletterMetadata, NewsletterUpdate, QueryIds, XWAPaths } from '../Types'
import type { NewsletterMetadata, NewsletterUpdate } from '../Types'
import { QueryIds, XWAPaths } from '../Types'
import { generateProfilePicture } from '../Utils/messages-media'
import { getBinaryNodeChild } from '../WABinary'
import { GroupsSocket } from './groups'
import type { GroupsSocket } from './groups'
import { executeWMexQuery as genericExecuteWMexQuery } from './mex'
const parseNewsletterCreateResponse = (response: NewsletterCreateResponse): NewsletterMetadata => {
+1 -1
View File
@@ -487,7 +487,7 @@ export const makeSocket = (config: SocketConfig) => {
attrs: {
jid: authState.creds.me.id,
stage: 'companion_hello',
// eslint-disable-next-line camelcase
should_show_push_notification: 'true'
},
content: [
-206
View File
@@ -1,206 +0,0 @@
import { AccountSettings, ChatMutation, Contact, InitialAppStateSyncOptions } from '../Types'
import { unixTimestampSeconds } from '../Utils'
import { processSyncAction } from '../Utils/chat-utils'
import logger from '../Utils/logger'
describe('App State Sync Tests', () => {
const me: Contact = { id: randomJid() }
// case when initial sync is off
it('should return archive=false event', () => {
const jid = randomJid()
const index = ['archive', jid]
const CASES: ChatMutation[][] = [
[
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: false,
messageRange: {
lastMessageTimestamp: unixTimestampSeconds()
}
}
}
}
}
],
[
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: true,
messageRange: {
lastMessageTimestamp: unixTimestampSeconds()
}
}
}
}
},
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: false,
messageRange: {
lastMessageTimestamp: unixTimestampSeconds()
}
}
}
}
}
]
]
for (const mutations of CASES) {
const events = processSyncAction(mutations, me, undefined, logger)
expect(events['chats.update']).toHaveLength(1)
const event = events['chats.update']?.[0]
expect(event.archive).toEqual(false)
}
})
// case when initial sync is on
// and unarchiveChats = true
it('should not fire any archive event', () => {
const jid = randomJid()
const index = ['archive', jid]
const now = unixTimestampSeconds()
const CASES: ChatMutation[][] = [
[
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: true,
messageRange: {
lastMessageTimestamp: now - 1
}
}
}
}
}
],
[
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: false,
messageRange: {
lastMessageTimestamp: now + 10
}
}
}
}
}
],
[
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: true,
messageRange: {
lastMessageTimestamp: now + 10
}
}
}
}
},
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: false,
messageRange: {
lastMessageTimestamp: now + 11
}
}
}
}
}
]
]
const ctx: InitialAppStateSyncOptions = {
recvChats: {
[jid]: { lastMsgRecvTimestamp: now }
},
accountSettings: { unarchiveChats: true }
}
for (const mutations of CASES) {
const events = processSyncActions(mutations, me, ctx, logger)
expect(events['chats.update']?.length).toBeFalsy()
}
})
// case when initial sync is on
// with unarchiveChats = true & unarchiveChats = false
it('should fire archive=true events', () => {
const jid = randomJid()
const index = ['archive', jid]
const now = unixTimestampSeconds()
const CASES: { settings: AccountSettings; mutations: ChatMutation[] }[] = [
{
settings: { unarchiveChats: true },
mutations: [
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: true,
messageRange: {
lastMessageTimestamp: now
}
}
}
}
}
]
},
{
settings: { unarchiveChats: false },
mutations: [
{
index,
syncAction: {
value: {
archiveChatAction: {
archived: true,
messageRange: {
lastMessageTimestamp: now - 10
}
}
}
}
}
]
}
]
for (const { mutations, settings } of CASES) {
const ctx: InitialAppStateSyncOptions = {
recvChats: {
[jid]: { lastMsgRecvTimestamp: now }
},
accountSettings: settings
}
const events = processSyncActions(mutations, me, ctx, logger)
expect(events['chats.update']).toHaveLength(1)
const event = events['chats.update']?.[0]
expect(event.archive).toEqual(true)
}
})
})
-330
View File
@@ -1,330 +0,0 @@
import { proto } from '../../WAProto'
import { Chat, WAMessageKey, WAMessageStatus, WAMessageStubType, WAMessageUpdate } from '../Types'
import { delay, generateMessageID, makeEventBuffer, toNumber, unixTimestampSeconds } from '../Utils'
import logger from '../Utils/logger'
import { randomJid } from './utils'
describe('Event Buffer Tests', () => {
let ev: ReturnType<typeof makeEventBuffer>
beforeEach(() => {
const _logger = logger.child({})
_logger.level = 'trace'
ev = makeEventBuffer(_logger)
})
it('should buffer a chat upsert & update event', async () => {
const chatId = randomJid()
const chats: Chat[] = []
ev.on('chats.upsert', c => chats.push(...c))
ev.on('chats.update', () => fail('should not emit update event'))
ev.buffer()
await Promise.all([
(async () => {
ev.buffer()
await delay(100)
ev.emit('chats.upsert', [{ id: chatId, conversationTimestamp: 123, unreadCount: 1 }])
const flushed = ev.flush()
expect(flushed).toBeFalsy()
})(),
(async () => {
ev.buffer()
await delay(200)
ev.emit('chats.update', [{ id: chatId, conversationTimestamp: 124, unreadCount: 1 }])
const flushed = ev.flush()
expect(flushed).toBeFalsy()
})()
])
const flushed = ev.flush()
expect(flushed).toBeTruthy()
expect(chats).toHaveLength(1)
expect(chats[0].conversationTimestamp).toEqual(124)
expect(chats[0].unreadCount).toEqual(2)
})
it('should overwrite a chats.delete event', async () => {
const chatId = randomJid()
const chats: Partial<Chat>[] = []
ev.on('chats.update', c => chats.push(...c))
ev.on('chats.delete', () => fail('not should have emitted'))
ev.buffer()
ev.emit('chats.update', [{ id: chatId, conversationTimestamp: 123, unreadCount: 1 }])
ev.emit('chats.delete', [chatId])
ev.emit('chats.update', [{ id: chatId, conversationTimestamp: 124, unreadCount: 1 }])
ev.flush()
expect(chats).toHaveLength(1)
})
it('should overwrite a chats.update event', async () => {
const chatId = randomJid()
const chatsDeleted: string[] = []
ev.on('chats.delete', c => chatsDeleted.push(...c))
ev.on('chats.update', () => fail('not should have emitted'))
ev.buffer()
ev.emit('chats.update', [{ id: chatId, conversationTimestamp: 123, unreadCount: 1 }])
ev.emit('chats.delete', [chatId])
ev.flush()
expect(chatsDeleted).toHaveLength(1)
})
it('should release a conditional update at the right time', async () => {
const chatId = randomJid()
const chatId2 = randomJid()
const chatsUpserted: Chat[] = []
const chatsSynced: Chat[] = []
ev.on('chats.upsert', c => chatsUpserted.push(...c))
ev.on('messaging-history.set', c => chatsSynced.push(...c.chats))
ev.on('chats.update', () => fail('not should have emitted'))
ev.buffer()
ev.emit('chats.update', [
{
id: chatId,
archived: true,
conditional(buff) {
if (buff.chatUpserts[chatId]) {
return true
}
}
}
])
ev.emit('chats.update', [
{
id: chatId2,
archived: true,
conditional(buff) {
if (buff.historySets.chats[chatId2]) {
return true
}
}
}
])
ev.flush()
ev.buffer()
ev.emit('chats.upsert', [
{
id: chatId,
conversationTimestamp: 123,
unreadCount: 1,
muteEndTime: 123
}
])
ev.emit('messaging-history.set', {
chats: [
{
id: chatId2,
conversationTimestamp: 123,
unreadCount: 1,
muteEndTime: 123
}
],
contacts: [],
messages: [],
isLatest: false
})
ev.flush()
expect(chatsUpserted).toHaveLength(1)
expect(chatsUpserted[0].id).toEqual(chatId)
expect(chatsUpserted[0].archived).toEqual(true)
expect(chatsUpserted[0].muteEndTime).toEqual(123)
expect(chatsSynced).toHaveLength(1)
expect(chatsSynced[0].id).toEqual(chatId2)
expect(chatsSynced[0].archived).toEqual(true)
})
it('should discard a conditional update', async () => {
const chatId = randomJid()
const chatsUpserted: Chat[] = []
ev.on('chats.upsert', c => chatsUpserted.push(...c))
ev.on('chats.update', () => fail('not should have emitted'))
ev.buffer()
ev.emit('chats.update', [
{
id: chatId,
archived: true,
conditional(buff) {
if (buff.chatUpserts[chatId]) {
return false
}
}
}
])
ev.emit('chats.upsert', [
{
id: chatId,
conversationTimestamp: 123,
unreadCount: 1,
muteEndTime: 123
}
])
ev.flush()
expect(chatsUpserted).toHaveLength(1)
expect(chatsUpserted[0].archived).toBeUndefined()
})
it('should overwrite a chats.update event with a history event', async () => {
const chatId = randomJid()
let chatRecv: Chat | undefined
ev.on('messaging-history.set', ({ chats }) => {
chatRecv = chats[0]
})
ev.on('chats.update', () => fail('not should have emitted'))
ev.buffer()
ev.emit('messaging-history.set', {
chats: [{ id: chatId, conversationTimestamp: 123, unreadCount: 1 }],
messages: [],
contacts: [],
isLatest: true
})
ev.emit('chats.update', [{ id: chatId, archived: true }])
ev.flush()
expect(chatRecv).toBeDefined()
expect(chatRecv?.archived).toBeTruthy()
})
it('should buffer message upsert events', async () => {
const messageTimestamp = unixTimestampSeconds()
const msg: proto.IWebMessageInfo = {
key: {
remoteJid: randomJid(),
id: generateMessageID(),
fromMe: false
},
messageStubType: WAMessageStubType.CIPHERTEXT,
messageTimestamp
}
const msgs: proto.IWebMessageInfo[] = []
ev.on('messages.upsert', c => {
msgs.push(...c.messages)
expect(c.type).toEqual('notify')
})
ev.buffer()
ev.emit('messages.upsert', { messages: [proto.WebMessageInfo.fromObject(msg)], type: 'notify' })
msg.messageTimestamp = unixTimestampSeconds() + 1
msg.messageStubType = undefined
msg.message = { conversation: 'Test' }
ev.emit('messages.upsert', { messages: [proto.WebMessageInfo.fromObject(msg)], type: 'notify' })
ev.emit('messages.update', [{ key: msg.key, update: { status: WAMessageStatus.READ } }])
ev.flush()
expect(msgs).toHaveLength(1)
expect(msgs[0].message).toBeTruthy()
expect(toNumber(msgs[0].messageTimestamp!)).toEqual(messageTimestamp)
expect(msgs[0].status).toEqual(WAMessageStatus.READ)
})
it('should buffer a message receipt update', async () => {
const msg: proto.IWebMessageInfo = {
key: {
remoteJid: randomJid(),
id: generateMessageID(),
fromMe: false
},
messageStubType: WAMessageStubType.CIPHERTEXT,
messageTimestamp: unixTimestampSeconds()
}
const msgs: proto.IWebMessageInfo[] = []
ev.on('messages.upsert', c => msgs.push(...c.messages))
ev.on('message-receipt.update', () => fail('should not emit'))
ev.buffer()
ev.emit('messages.upsert', { messages: [proto.WebMessageInfo.fromObject(msg)], type: 'notify' })
ev.emit('message-receipt.update', [
{
key: msg.key,
receipt: {
userJid: randomJid(),
readTimestamp: unixTimestampSeconds()
}
}
])
ev.flush()
expect(msgs).toHaveLength(1)
expect(msgs[0].userReceipt).toHaveLength(1)
})
it('should buffer multiple status updates', async () => {
const key: WAMessageKey = {
remoteJid: randomJid(),
id: generateMessageID(),
fromMe: false
}
const msgs: WAMessageUpdate[] = []
ev.on('messages.update', c => msgs.push(...c))
ev.buffer()
ev.emit('messages.update', [{ key, update: { status: WAMessageStatus.DELIVERY_ACK } }])
ev.emit('messages.update', [{ key, update: { status: WAMessageStatus.READ } }])
ev.flush()
expect(msgs).toHaveLength(1)
expect(msgs[0].update.status).toEqual(WAMessageStatus.READ)
})
it('should remove chat unread counter', async () => {
const msg: proto.IWebMessageInfo = {
key: {
remoteJid: '12345@s.whatsapp.net',
id: generateMessageID(),
fromMe: false
},
message: {
conversation: 'abcd'
},
messageTimestamp: unixTimestampSeconds()
}
const chats: Partial<Chat>[] = []
ev.on('chats.update', c => chats.push(...c))
ev.buffer()
ev.emit('messages.upsert', { messages: [proto.WebMessageInfo.fromObject(msg)], type: 'notify' })
ev.emit('chats.update', [{ id: msg.key.remoteJid!, unreadCount: 1, conversationTimestamp: msg.messageTimestamp }])
ev.emit('messages.update', [{ key: msg.key, update: { status: WAMessageStatus.READ } }])
ev.flush()
expect(chats[0].unreadCount).toBeUndefined()
})
})
-77
View File
@@ -1,77 +0,0 @@
import { addTransactionCapability, delay } from '../Utils'
import logger from '../Utils/logger'
import { makeMockSignalKeyStore } from './utils'
logger.level = 'trace'
describe('Key Store w Transaction Tests', () => {
const rawStore = makeMockSignalKeyStore()
const store = addTransactionCapability(rawStore, logger, {
maxCommitRetries: 1,
delayBetweenTriesMs: 10
})
it('should use transaction cache when mutated', async () => {
const key = '123'
const value = new Uint8Array(1)
const ogGet = rawStore.get
await store.transaction(async () => {
await store.set({ session: { [key]: value } })
rawStore.get = () => {
throw new Error('should not have been called')
}
const { [key]: stored } = await store.get('session', [key])
expect(stored).toEqual(new Uint8Array(1))
})
rawStore.get = ogGet
})
it('should not commit a failed transaction', async () => {
const key = 'abcd'
await expect(
store.transaction(async () => {
await store.set({ session: { [key]: new Uint8Array(1) } })
throw new Error('fail')
})
).rejects.toThrowError('fail')
const { [key]: stored } = await store.get('session', [key])
expect(stored).toBeUndefined()
})
it('should handle overlapping transactions', async () => {
// promise to let transaction 2
// know that transaction 1 has started
let promiseResolve: () => void
const promise = new Promise<void>(resolve => {
promiseResolve = resolve
})
store.transaction(async () => {
await store.set({
session: {
'1': new Uint8Array(1)
}
})
// wait for the other transaction to start
await delay(5)
// reolve the promise to let the other transaction continue
promiseResolve()
})
await store.transaction(async () => {
await promise
await delay(5)
expect(store.isInTransaction()).toBe(true)
})
expect(store.isInTransaction()).toBe(false)
// ensure that the transaction were committed
const { ['1']: stored } = await store.get('session', ['1'])
expect(stored).toEqual(new Uint8Array(1))
})
})
-163
View File
@@ -1,163 +0,0 @@
import { makeLibSignalRepository } from '../Signal/libsignal'
import { SignalAuthState, SignalDataTypeMap } from '../Types'
import { Curve, generateRegistrationId, generateSignalPubKey, signedKeyPair } from '../Utils'
// TODO: should move to libsignal
describe('Signal Tests', () => {
it('should correctly encrypt/decrypt 1 message', async () => {
const user1 = makeUser()
const user2 = makeUser()
const msg = Buffer.from('hello there!')
await prepareForSendingMessage(user1, user2)
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
expect(dec).toEqual(msg)
})
it('should correctly override a session', async () => {
const user1 = makeUser()
const user2 = makeUser()
const msg = Buffer.from('hello there!')
for (let preKeyId = 2; preKeyId <= 3; preKeyId++) {
await prepareForSendingMessage(user1, user2, preKeyId)
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
expect(dec).toEqual(msg)
}
})
it('should correctly encrypt/decrypt multiple messages', async () => {
const user1 = makeUser()
const user2 = makeUser()
const msg = Buffer.from('hello there!')
await prepareForSendingMessage(user1, user2)
for (let i = 0; i < 10; i++) {
const result = await user1.repository.encryptMessage({ jid: user2.jid, data: msg })
const dec = await user2.repository.decryptMessage({ jid: user1.jid, ...result })
expect(dec).toEqual(msg)
}
})
it('should encrypt/decrypt messages from group', async () => {
const groupId = '123456@g.us'
const participants = [...Array(5)].map(makeUser)
const msg = Buffer.from('hello there!')
const sender = participants[0]
const enc = await sender.repository.encryptGroupMessage({
group: groupId,
meId: sender.jid,
data: msg
})
for (const participant of participants) {
if (participant === sender) {
continue
}
await participant.repository.processSenderKeyDistributionMessage({
item: {
groupId,
axolotlSenderKeyDistributionMessage: enc.senderKeyDistributionMessage
},
authorJid: sender.jid
})
const dec = await participant.repository.decryptGroupMessage({
group: groupId,
authorJid: sender.jid,
msg: enc.ciphertext
})
expect(dec).toEqual(msg)
}
})
})
type User = ReturnType<typeof makeUser>
function makeUser() {
const store = makeTestAuthState()
const jid = `${Math.random().toString().replace('.', '')}@s.whatsapp.net`
const repository = makeLibSignalRepository(store)
return { store, jid, repository }
}
async function prepareForSendingMessage(sender: User, receiver: User, preKeyId = 2) {
const preKey = Curve.generateKeyPair()
await sender.repository.injectE2ESession({
jid: receiver.jid,
session: {
registrationId: receiver.store.creds.registrationId,
identityKey: generateSignalPubKey(receiver.store.creds.signedIdentityKey.public),
signedPreKey: {
keyId: receiver.store.creds.signedPreKey.keyId,
publicKey: generateSignalPubKey(receiver.store.creds.signedPreKey.keyPair.public),
signature: receiver.store.creds.signedPreKey.signature
},
preKey: {
keyId: preKeyId,
publicKey: generateSignalPubKey(preKey.public)
}
}
})
await receiver.store.keys.set({
'pre-key': {
[preKeyId]: preKey
}
})
}
function makeTestAuthState(): SignalAuthState {
const identityKey = Curve.generateKeyPair()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const store: { [_: string]: any } = {}
return {
creds: {
signedIdentityKey: identityKey,
registrationId: generateRegistrationId(),
signedPreKey: signedKeyPair(identityKey, 1)
},
keys: {
get(type, ids) {
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
for (const id of ids) {
const item = store[getUniqueId(type, id)]
if (typeof item !== 'undefined') {
data[id] = item
}
}
return data
},
set(data) {
for (const type in data) {
for (const id in data[type]) {
store[getUniqueId(type, id)] = data[type][id]
}
}
}
}
}
function getUniqueId(type: string, id: string) {
return `${type}.${id}`
}
}
-75
View File
@@ -1,75 +0,0 @@
import { readFileSync } from 'fs'
import { proto } from '../../WAProto'
import { DownloadableMessage, MediaType } from '../Types'
import { downloadContentFromMessage } from '../Utils'
jest.setTimeout(20_000)
type TestVector = {
type: MediaType
message: DownloadableMessage
plaintext: Buffer
}
const TEST_VECTORS: TestVector[] = [
{
type: 'image',
message: proto.Message.ImageMessage.decode(
Buffer.from(
'Ck1odHRwczovL21tZy53aGF0c2FwcC5uZXQvZC9mL0FwaHR4WG9fWXZZcDZlUVNSa0tjOHE5d2ozVUpleWdoY3poM3ExX3I0ektnLmVuYxIKaW1hZ2UvanBlZyIgKTuVFyxDc6mTm4GXPlO3Z911Wd8RBeTrPLSWAEdqW8MomcUBQiB7wH5a4nXMKyLOT0A2nFgnnM/DUH8YjQf8QtkCIekaSkogTB+BXKCWDFrmNzozY0DCPn0L4VKd7yG1ZbZwbgRhzVc=',
'base64'
)
),
plaintext: readFileSync('./Media/cat.jpeg')
},
{
type: 'image',
message: proto.Message.ImageMessage.decode(
Buffer.from(
'Ck1odHRwczovL21tZy53aGF0c2FwcC5uZXQvZC9mL0Ftb2tnWkphNWF6QWZxa3dVRzc0eUNUdTlGeWpjMmd5akpqcXNmMUFpZEU5LmVuYxIKaW1hZ2UvanBlZyIg8IS5TQzdzcuvcR7F8HMhWnXmlsV+GOo9JE1/t2k+o9Yoz6o6QiA7kDk8j5KOEQC0kDFE1qW7lBBDYhm5z06N3SirfUj3CUog/CjYF8e670D5wUJwWv2B2mKzDEo8IJLStDv76YmtPfs=',
'base64'
)
),
plaintext: readFileSync('./Media/icon.png')
}
]
describe('Media Download Tests', () => {
it('should download a full encrypted media correctly', async () => {
for (const { type, message, plaintext } of TEST_VECTORS) {
const readPipe = await downloadContentFromMessage(message, type)
let buffer = Buffer.alloc(0)
for await (const read of readPipe) {
buffer = Buffer.concat([buffer, read])
}
expect(buffer).toEqual(plaintext)
}
})
it('should download an encrypted media correctly piece', async () => {
for (const { type, message, plaintext } of TEST_VECTORS) {
// check all edge cases
const ranges = [
{ startByte: 51, endByte: plaintext.length - 100 }, // random numbers
{ startByte: 1024, endByte: 2038 }, // larger random multiples of 16
{ startByte: 1, endByte: plaintext.length - 1 } // borders
]
for (const range of ranges) {
const readPipe = await downloadContentFromMessage(message, type, range)
let buffer = Buffer.alloc(0)
for await (const read of readPipe) {
buffer = Buffer.concat([buffer, read])
}
const hex = buffer.toString('hex')
const expectedHex = plaintext.slice(range.startByte || 0, range.endByte || undefined).toString('hex')
expect(hex).toBe(expectedHex)
console.log('success on ', range)
}
}
})
})
-34
View File
@@ -1,34 +0,0 @@
import { WAMessageContent } from '../Types'
import { normalizeMessageContent } from '../Utils'
describe('Messages Tests', () => {
it('should correctly unwrap messages', () => {
const CONTENT = { imageMessage: {} }
expectRightContent(CONTENT)
expectRightContent({
ephemeralMessage: { message: CONTENT }
})
expectRightContent({
viewOnceMessage: {
message: {
ephemeralMessage: { message: CONTENT }
}
}
})
expectRightContent({
viewOnceMessage: {
message: {
viewOnceMessageV2: {
message: {
ephemeralMessage: { message: CONTENT }
}
}
}
}
})
function expectRightContent(content: WAMessageContent) {
expect(normalizeMessageContent(content)).toHaveProperty('imageMessage')
}
})
})
-35
View File
@@ -1,35 +0,0 @@
import { jidEncode } from '../WABinary'
export function randomJid() {
return jidEncode(Math.floor(Math.random() * 1000000), Math.random() < 0.5 ? 's.whatsapp.net' : 'g.us')
}
export function makeMockSignalKeyStore(): SignalKeyStore {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const store: { [_: string]: any } = {}
return {
get(type, ids) {
const data: { [_: string]: SignalDataTypeMap[typeof type] } = {}
for (const id of ids) {
const item = store[getUniqueId(type, id)]
if (typeof item !== 'undefined') {
data[id] = item
}
}
return data
},
set(data) {
for (const type in data) {
for (const id in data[type]) {
store[getUniqueId(type, id)] = data[type][id]
}
}
}
}
function getUniqueId(type: string, id: string) {
return `${type}.${id}`
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ export interface GroupMetadata {
addressingMode: 'pn' | 'lid'
owner: string | undefined
ownerJid?: string | undefined
owner_country_code: string
owner_country_code?: string | undefined
subject: string
/** group subject owner */
subjectOwner?: string
+6 -4
View File
@@ -32,6 +32,7 @@ import type { ILogger } from './logger'
const getTmpFilesDirectory = () => tmpdir()
const getImageProcessingLibrary = async () => {
//@ts-ignore
const [jimp, sharp] = await Promise.all([import('jimp').catch(() => {}), import('sharp').catch(() => {})])
if (sharp) {
@@ -152,7 +153,7 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
}
}
} else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
const jimp = await lib.jimp.default.Jimp.read(bufferOrFilePath)
const jimp = await (lib.jimp.Jimp as any).read(bufferOrFilePath)
const dimensions = {
width: jimp.width,
height: jimp.height
@@ -200,7 +201,7 @@ export const generateProfilePicture = async (
})
.toBuffer()
} else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
const jimp = await lib.jimp.default.Jimp.read(buffer)
const jimp = await (lib.jimp.Jimp as any).read(buffer)
const min = Math.min(jimp.width, jimp.height)
const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min })
@@ -242,7 +243,8 @@ export async function getAudioDuration(buffer: Buffer | string | Readable) {
*/
export async function getAudioWaveform(buffer: Buffer | string | Readable, logger?: ILogger) {
try {
const { default: decoder } = await eval("import('audio-decode')")
// @ts-ignore
const { default: decoder } = await import('audio-decode')
let audioData: Buffer
if (Buffer.isBuffer(buffer)) {
audioData = buffer
@@ -311,7 +313,7 @@ export const getStream = async (item: WAMediaUpload, opts?: AxiosRequestConfig)
const urlStr = item.url.toString()
if (urlStr.startsWith('data:')) {
const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
const buffer = Buffer.from(urlStr.split(',')[1]!, 'base64')
return { stream: toReadable(buffer), type: 'buffer' } as const
}
+3 -2
View File
@@ -30,7 +30,7 @@ import {
generateThumbnail,
getAudioDuration,
getAudioWaveform,
getRawMediaUploadData,
getRawMediaUploadData,
type MediaDownloadOptions
} from './messages-media'
@@ -177,7 +177,8 @@ export const prepareWAMessageMedia = async (
await fs.unlink(filePath)
const obj = WAProto.Message.fromObject({
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
// todo: add more support here
[`${mediaType}Message`]: (MessageTypeProto as any)[mediaType].fromObject({
url: mediaUrl,
directPath,
fileSha256,
+10681 -8088
View File
File diff suppressed because it is too large Load Diff