initial commit
commit
fb7610845e
|
@ -0,0 +1,2 @@
|
|||
node_modules/**
|
||||
dist/**
|
|
@ -0,0 +1,7 @@
|
|||
Copyright 2023 sati.ac
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,25 @@
|
|||
# sati-js - javascript client for sati.ac
|
||||
[![NPM version](https://badge.fury.io/js/sati.svg)](https://www.npmjs.com/package/sati)
|
||||
![Downloads](https://img.shields.io/npm/dm/sati.svg?style=flat)
|
||||
|
||||
## usage example
|
||||
```ts
|
||||
const sati = new Sati({
|
||||
token: 'your token here'
|
||||
})
|
||||
await sati.init() // you must call the init method after construction
|
||||
|
||||
const balance = await sati.getBalance()
|
||||
console.log(balance)
|
||||
|
||||
// first argument - task type, second - task parameters
|
||||
// supported tasks: https://sati.ac/docs/tasks
|
||||
const task = await sati.solve('Turnstile', {
|
||||
siteKey: '0x4AAAAAAAHMEd1rGJs9qy-0',
|
||||
pageUrl: 'https://polygon.sati.ac/Turnstile'
|
||||
})
|
||||
|
||||
console.log(task, task.result.token)
|
||||
|
||||
sati.close() // you must call close method after you've done
|
||||
```
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "sati",
|
||||
"version": "0.1.0",
|
||||
"description": "next generation anti-captcha",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://git.sati.ac/sati.ac/sati-js"
|
||||
},
|
||||
"keywords": [
|
||||
"sati.ac",
|
||||
"anti-captcha",
|
||||
"anticaptcha",
|
||||
"captcha"
|
||||
],
|
||||
"files": [
|
||||
"dist/",
|
||||
"!**/*.tsbuildinfo"
|
||||
],
|
||||
"main": "dist/sati.node.cjs.js",
|
||||
"unpkg": "dist/sati.web.umd.js",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && webpack"
|
||||
},
|
||||
"author": "",
|
||||
"dependencies": {
|
||||
"@types/node": "^20.1.1",
|
||||
"@types/ws": "^8.5.4",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^5.82.0",
|
||||
"webpack-cli": "^5.1.1"
|
||||
},
|
||||
"types": "./dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "dist/sati.web.esm.mjs",
|
||||
"require": "dist/sati.web.umd.js",
|
||||
"types": "dist/src/index.d.ts"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import { EventEmitter } from './helpers/EventEmitter'
|
||||
import { SatiError } from './SatiError'
|
||||
import { SatiSocket, SocketState } from './SatiSocket'
|
||||
import { events, methods, tasks, Task } from './types'
|
||||
|
||||
/**
|
||||
* high level api wrapper
|
||||
* @example
|
||||
* const sati = new Sati({
|
||||
* token: 'your token here'
|
||||
* })
|
||||
* await sati.init() // you must call the init method after construction
|
||||
*
|
||||
* const task = await sati.solve('Turnstile', {
|
||||
* siteKey: '0x4AAAAAAAHMEd1rGJs9qy-0',
|
||||
* pageUrl: 'https://polygon.sati.ac/Turnstile'
|
||||
* })
|
||||
*
|
||||
* console.log(task, task.result.token)
|
||||
*
|
||||
* sati.close() // you must call close method after you've done
|
||||
*/
|
||||
export class Sati extends EventEmitter<events> {
|
||||
private socket: SatiSocket
|
||||
private awaitedTasks: {
|
||||
[ index: number ]: {
|
||||
resolve(data: any): void,
|
||||
reject(reason: any): void
|
||||
}
|
||||
} = Object.create(null)
|
||||
|
||||
/** @param token your api token. get it at https://sati.ac/dashboard */
|
||||
constructor({ token }: { token: string }) {
|
||||
super()
|
||||
this.socket = new SatiSocket(token)
|
||||
this.socket.on('event', ({ type, data }) => {
|
||||
this.emit(type as keyof events, data)
|
||||
})
|
||||
this.socket.on('stateChange', ({ state, error }) => {
|
||||
switch(state) {
|
||||
case SocketState.connected:
|
||||
// as of socket was disconnected, there might be missed events
|
||||
// so we need to recheck the state of each awaited task
|
||||
for(const [ id, awaited ] of Object.entries(this.awaitedTasks)) {
|
||||
this.call('getTask', { id: +id }).then(this.handleTaskResult.bind(this), err => {
|
||||
awaited.reject(err)
|
||||
delete this.awaitedTasks[+id]
|
||||
})
|
||||
}
|
||||
case SocketState.unrecoverable:
|
||||
// socket got an unrecoverable error, so it will never be connected again
|
||||
for(const awaited of Object.values(this.awaitedTasks)) {
|
||||
awaited.reject(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.on('taskUpdate', this.handleTaskResult.bind(this))
|
||||
}
|
||||
|
||||
private handleTaskResult(task: Task) {
|
||||
if(!(task.id in this.awaitedTasks)) return
|
||||
if(task.state !== 'error' && task.state !== 'success') return
|
||||
const awaited = this.awaitedTasks[task.id]
|
||||
|
||||
if(task.state === 'error') {
|
||||
awaited.reject(new SatiError(`unable to solve ${task.type} task #${task.id}`))
|
||||
} else if(task.state === 'success') {
|
||||
awaited.resolve(task)
|
||||
}
|
||||
delete this.awaitedTasks[task.id]
|
||||
}
|
||||
|
||||
/** connects to the socket, must be called before any other methods */
|
||||
public async init() {
|
||||
await this.socket.connect()
|
||||
}
|
||||
|
||||
/** closes the socket */
|
||||
public close() {
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
/** calls the api method, use top-level wrappers for them instead */
|
||||
public async call<M extends keyof methods>(method: M, data: methods[M]['request']): Promise<methods[M]['response']> {
|
||||
const resp = await this.socket.send('call', { method, data })
|
||||
|
||||
if(!resp.success) {
|
||||
throw new SatiError(resp.data.description)
|
||||
}
|
||||
|
||||
return resp.data
|
||||
}
|
||||
|
||||
/**
|
||||
* creates task, then waits for the result
|
||||
* @throws {SatiError} if unable to solve
|
||||
*/
|
||||
public async solve<T extends keyof tasks>(type: T, data: tasks[T]['params']): Promise<Task<T, 'success'>> {
|
||||
const task = await this.call('createTask', { type, data })
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.awaitedTasks[task.id] = { resolve, reject }
|
||||
})
|
||||
}
|
||||
|
||||
public async getBalance() {
|
||||
return (await this.call('getBalance', {})).balance
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export class SatiError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(`sati: ${msg}`)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
import { EventEmitter } from './helpers/EventEmitter'
|
||||
import { SocketAdapter } from '%env%'
|
||||
import { SatiError } from './SatiError'
|
||||
import { endpoint } from './config'
|
||||
|
||||
interface WebSocketMessage {
|
||||
id: number
|
||||
type: string
|
||||
data: any
|
||||
to?: number
|
||||
}
|
||||
|
||||
export enum SocketState {
|
||||
connected,
|
||||
disconnected,
|
||||
unrecoverable
|
||||
}
|
||||
|
||||
/** low-level api wrapper */
|
||||
export class SatiSocket extends EventEmitter<{
|
||||
event: {
|
||||
type: string,
|
||||
data: any
|
||||
},
|
||||
stateChange: {
|
||||
state: SocketState,
|
||||
error: Error | null
|
||||
}
|
||||
}> {
|
||||
private token: string
|
||||
private socket: SocketAdapter | null = null
|
||||
private error: Error | null = null
|
||||
private state: SocketState = SocketState.disconnected
|
||||
|
||||
private idCounter = 0
|
||||
private awaitedMessages: Record<number, {
|
||||
resolve: (data: any) => void
|
||||
reject: (error: any) => void
|
||||
}> = Object.create(null)
|
||||
private queue: [(data: any) => void, string, any][] = []
|
||||
|
||||
constructor(token: string) {
|
||||
super()
|
||||
this.token = token
|
||||
}
|
||||
|
||||
private setState(state: SocketState, error: Error | null = null) {
|
||||
this.state = state
|
||||
this.error = error
|
||||
this.emit('stateChange', { state, error })
|
||||
}
|
||||
|
||||
public async connect() {
|
||||
if(this.socket || this.state !== SocketState.disconnected) return;
|
||||
this.idCounter = 0
|
||||
this.socket = new SocketAdapter(endpoint)
|
||||
this.socket.once('close', this.onSocketClose.bind(this))
|
||||
this.socket.on('message', this.onMessage.bind(this))
|
||||
await new Promise((res, rej) => {
|
||||
this.socket?.once('open', res)
|
||||
this.socket?.once('error', rej)
|
||||
})
|
||||
const resp = await this.unqueuedSend('auth', { token: this.token })
|
||||
if(!resp.success) {
|
||||
const error = new SatiError('invalid auth token')
|
||||
this.setState(SocketState.unrecoverable, error)
|
||||
this.socket?.close()
|
||||
throw error
|
||||
}
|
||||
this.setState(SocketState.connected)
|
||||
await this.flushQueue()
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.setState(SocketState.unrecoverable, new SatiError('socket was closed'))
|
||||
this.socket?.close()
|
||||
}
|
||||
|
||||
private flushQueue() {
|
||||
const promises = []
|
||||
for(const [ cb, type, data ] of this.queue) {
|
||||
promises.push(this.unqueuedSend(type, data).then(cb))
|
||||
}
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
private onSocketClose() {
|
||||
this.socket = null
|
||||
|
||||
for(const awaited of Object.values(this.awaitedMessages)) {
|
||||
// we don't want to resend them, because it may cause task double-creation
|
||||
awaited.reject(new SatiError('socket gone'))
|
||||
}
|
||||
this.awaitedMessages = Object.create(null)
|
||||
|
||||
if(this.state !== SocketState.unrecoverable) {
|
||||
this.setState(SocketState.disconnected)
|
||||
setTimeout(() => {
|
||||
this.connect().catch(err => {
|
||||
console?.error?.('sati: error while reconnecting:', err)
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
private async unqueuedSend(type: string, data: any) {
|
||||
if(this.state === SocketState.unrecoverable) throw this.error
|
||||
if(!this.socket) return;
|
||||
const id = ++this.idCounter
|
||||
|
||||
this.socket.send(JSON.stringify({ id, type, data }))
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
this.awaitedMessages[id] = { resolve, reject }
|
||||
})
|
||||
}
|
||||
|
||||
public send(type: string, data: any) {
|
||||
switch(this.state) {
|
||||
case SocketState.connected:
|
||||
return this.unqueuedSend(type, data)
|
||||
case SocketState.disconnected:
|
||||
return new Promise(resolve => {
|
||||
this.queue.push([ resolve, type, data ])
|
||||
})
|
||||
case SocketState.unrecoverable:
|
||||
throw this.error
|
||||
}
|
||||
}
|
||||
|
||||
private onMessage(message: string) {
|
||||
const parsed: WebSocketMessage = JSON.parse(message)
|
||||
if(parsed.type === 'event') {
|
||||
if(parsed.data.type === 'tokenReissue') {
|
||||
this.setState(SocketState.unrecoverable, new SatiError('token was reissued'))
|
||||
this.socket?.close()
|
||||
}
|
||||
|
||||
this.emit('event', parsed.data)
|
||||
return
|
||||
}
|
||||
|
||||
if(parsed.to && parsed.to in this.awaitedMessages) {
|
||||
this.awaitedMessages[parsed.to].resolve(parsed.data)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const endpoint = 'wss://api.sati.ac/ws'
|
|
@ -0,0 +1,36 @@
|
|||
import { WebSocket, RawData } from 'ws'
|
||||
import { SocketAdapter } from '../../helpers/SocketAdapter'
|
||||
|
||||
class NodeSocketAdapter extends SocketAdapter {
|
||||
private socket: WebSocket
|
||||
|
||||
constructor(url: string) {
|
||||
super()
|
||||
this.socket = new WebSocket(url)
|
||||
this.socket.on('message', this.emitMessage.bind(this))
|
||||
this.socket.on('error', err => this.emit('error', err))
|
||||
this.socket.on('open', () => this.emit('open', null))
|
||||
this.socket.on('close', () => this.emit('close', null))
|
||||
}
|
||||
|
||||
public send(message: string) {
|
||||
this.socket.send(message)
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
private emitMessage(message: RawData) {
|
||||
if(Array.isArray(message)) {
|
||||
message = Buffer.concat(message)
|
||||
}
|
||||
if(message instanceof ArrayBuffer) {
|
||||
message = Buffer.from(message)
|
||||
}
|
||||
|
||||
this.emit('message', message.toString())
|
||||
}
|
||||
}
|
||||
|
||||
export { NodeSocketAdapter as SocketAdapter }
|
|
@ -0,0 +1,23 @@
|
|||
import { SocketAdapter } from '../../helpers/SocketAdapter'
|
||||
|
||||
class BrowserSocketAdapter extends SocketAdapter {
|
||||
private socket: WebSocket
|
||||
constructor(url: string) {
|
||||
super()
|
||||
this.socket = new WebSocket(url)
|
||||
this.socket.addEventListener('message', e => this.emit('message', e.data))
|
||||
this.socket.addEventListener('error', e => this.emit('error', e))
|
||||
this.socket.addEventListener('open', () => this.emit('open', null))
|
||||
this.socket.addEventListener('close', () => this.emit('close', null))
|
||||
}
|
||||
|
||||
public send(message: string) {
|
||||
this.socket.send(message)
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
export { BrowserSocketAdapter as SocketAdapter }
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* fully typed node-like event emitter
|
||||
* usage: `EventEmitter<{ event: eventDataType }>`
|
||||
*/
|
||||
export class EventEmitter<T extends {} = {}> {
|
||||
private listeners: { [E in keyof T]: Set<((data: T[E]) => void)> } = Object.create(null)
|
||||
|
||||
public on<E extends keyof T>(event: E, listener: ((data: T[E]) => void)) {
|
||||
this.listeners[event] ||= new Set()
|
||||
this.listeners[event].add(listener)
|
||||
}
|
||||
|
||||
public off<E extends keyof T>(event: E, listener: ((data: T[E]) => void)) {
|
||||
this.listeners[event]?.delete(listener)
|
||||
if(this.listeners[event]?.size === 0) {
|
||||
delete this.listeners[event]
|
||||
}
|
||||
}
|
||||
|
||||
public once<E extends keyof T>(event: E, listener: ((data: T[E]) => void)) {
|
||||
const wrapped = (data: T[E]) => {
|
||||
this.off(event, wrapped)
|
||||
listener(data)
|
||||
}
|
||||
|
||||
this.on(event, wrapped)
|
||||
}
|
||||
|
||||
protected emit<E extends keyof T>(event: E, data: T[E]) {
|
||||
if(!this.listeners[event]) return
|
||||
for(const listener of this.listeners[event]) {
|
||||
listener(data)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { EventEmitter } from '../helpers/EventEmitter'
|
||||
|
||||
export interface SocketAdapterConstructor {
|
||||
new(url: string): SocketAdapter
|
||||
}
|
||||
|
||||
/** cross-platform websocket interface */
|
||||
export abstract class SocketAdapter extends EventEmitter<{
|
||||
message: string,
|
||||
open: null,
|
||||
close: null,
|
||||
error: any
|
||||
}> {
|
||||
public abstract send(message: string): void
|
||||
public abstract close(): void
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './Sati'
|
||||
export * from './SatiError'
|
||||
export type { Task } from './types'
|
|
@ -0,0 +1,72 @@
|
|||
export type tasks = {
|
||||
Turnstile: {
|
||||
params: {
|
||||
siteKey: string,
|
||||
pageUrl: string,
|
||||
action?: string,
|
||||
cData?: string
|
||||
},
|
||||
result: {
|
||||
token: string
|
||||
}
|
||||
},
|
||||
ReCaptcha2: {
|
||||
params: {
|
||||
siteKey: string,
|
||||
pageUrl: string
|
||||
},
|
||||
result: {
|
||||
token: string
|
||||
}
|
||||
},
|
||||
FunCaptcha: {
|
||||
params: {
|
||||
siteKey: string,
|
||||
pageUrl: string,
|
||||
data: Record<string, string>
|
||||
serviceUrl?: string
|
||||
},
|
||||
result: {
|
||||
token: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type methods = {
|
||||
getBalance: {
|
||||
request: {},
|
||||
response: {
|
||||
balance: string
|
||||
}
|
||||
},
|
||||
createTask: {
|
||||
request: {
|
||||
type: keyof tasks,
|
||||
data: tasks[keyof tasks]['params']
|
||||
},
|
||||
response: Task
|
||||
},
|
||||
getTask: {
|
||||
request: {
|
||||
id: number
|
||||
},
|
||||
response: Task
|
||||
}
|
||||
}
|
||||
|
||||
export type events = {
|
||||
taskUpdate: Task,
|
||||
tokenReissue: {}
|
||||
}
|
||||
|
||||
export type TaskState = 'queued' | 'processing' | 'error' | 'success'
|
||||
|
||||
export type Task<T extends keyof tasks = keyof tasks, S extends TaskState = TaskState> = {
|
||||
id: number,
|
||||
type: T,
|
||||
creationDate: string,
|
||||
cost: string
|
||||
state: S,
|
||||
} & (S extends 'success'
|
||||
? { result: tasks[T]['result'] }
|
||||
: { result: null })
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"allowJs": false,
|
||||
"declaration": true,
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"types": [ "node" ],
|
||||
"rootDir": ".",
|
||||
"incremental": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
"%env%": ["./src/env/node"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
const path = require('path')
|
||||
|
||||
const build = env => ({
|
||||
mode: 'production',
|
||||
entry: './src/index.ts',
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.ts$/,
|
||||
use: {
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
module: 'esnext',
|
||||
paths: {
|
||||
'%env%': [`./src/env/${env}`]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
exclude: /node_modules/
|
||||
}],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'%env%': path.resolve(__dirname, `src/env/${env}`)
|
||||
},
|
||||
extensions: [ '.ts', '.js' ]
|
||||
},
|
||||
})
|
||||
|
||||
module.exports = [{
|
||||
// prebuilt binary, for browser usage
|
||||
...build('web'),
|
||||
output: {
|
||||
library: {
|
||||
name: 'Sati',
|
||||
type: 'umd'
|
||||
},
|
||||
filename: `sati.web.umd.js`
|
||||
}
|
||||
}, {
|
||||
// prebuilt binary with external dependencies, for browser usage with bundlers
|
||||
...build('web'),
|
||||
output: {
|
||||
library: {
|
||||
type: 'module'
|
||||
},
|
||||
filename: `sati.web.esm.js`
|
||||
},
|
||||
externals: /^[^\.%]/,
|
||||
experiments: {
|
||||
outputModule: true
|
||||
}
|
||||
}, {
|
||||
// prebuilt node version
|
||||
...build('node'),
|
||||
output: {
|
||||
library: {
|
||||
type: 'commonjs2'
|
||||
},
|
||||
filename: `sati.node.cjs.js`
|
||||
},
|
||||
externals: /^[^\.%]/,
|
||||
optimization: {
|
||||
minimize: false
|
||||
}
|
||||
}]
|
Loading…
Reference in New Issue