├── .prettierignore ├── .github ├── CODEOWNERS ├── dependabot.yml ├── workflows │ ├── automerge.yml │ └── publish.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── public ├── icons │ ├── wtf16.png │ ├── wtf32.png │ ├── wtf48.png │ ├── wtf64.png │ ├── wtf1024.png │ ├── wtf128.png │ ├── wtf256.png │ ├── wtf512.png │ └── wtf.svg ├── popup.html ├── options.html ├── manifest.json ├── schema.json └── _locales │ ├── en │ └── messages.json │ └── pt_BR │ └── messages.json ├── .husky └── pre-commit ├── .yarnrc.yml ├── src ├── types │ ├── CountryCode.ts │ ├── Attachment.ts │ ├── Log.ts │ ├── ChromeMessageTypes.ts │ ├── Message.ts │ ├── QueueStatus.ts │ └── ChromeMessageContentTypes.ts ├── index.css ├── content_script.ts ├── options.tsx ├── components │ ├── atoms │ │ ├── ControlFactory.tsx │ │ └── Button.tsx │ ├── molecules │ │ ├── Box.tsx │ │ └── SelectCountryCode.tsx │ └── organisms │ │ ├── LogTable.tsx │ │ ├── MessageForm.tsx │ │ └── MessageButtonsForm.tsx ├── utils │ ├── AsyncEventQueue.ts │ ├── AsyncStorageManager.ts │ └── AsyncChromeMessageManager.ts ├── wa-js.ts ├── popup.tsx ├── countryCodes.en.json └── countryCodes.pt.json ├── tests ├── wa-js.test.ts-snapshots │ ├── demo.gif │ ├── demo.mp3 │ ├── sample.mp4 │ └── sample.pdf ├── fixtures.ts └── wa-js.test.ts ├── .vscode ├── extensions.json └── settings.json ├── postcss.config.js ├── tailwind.config.js ├── LICENSE ├── webpack.config.js ├── NOTICE ├── .eslintrc ├── package.json ├── Jenkinsfile ├── playwright.config.ts ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md ├── CONTRIBUTING.md ├── .gitattributes └── tsconfig.json /.prettierignore: -------------------------------------------------------------------------------- 1 | tsconfig.json -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @marcosvrs -------------------------------------------------------------------------------- /public/icons/wtf16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf16.png -------------------------------------------------------------------------------- /public/icons/wtf32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf32.png -------------------------------------------------------------------------------- /public/icons/wtf48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf48.png -------------------------------------------------------------------------------- /public/icons/wtf64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf64.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /public/icons/wtf1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf1024.png -------------------------------------------------------------------------------- /public/icons/wtf128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf128.png -------------------------------------------------------------------------------- /public/icons/wtf256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf256.png -------------------------------------------------------------------------------- /public/icons/wtf512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/public/icons/wtf512.png -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableImmutableInstalls: false 2 | 3 | enableScripts: false 4 | 5 | nodeLinker: pnp 6 | -------------------------------------------------------------------------------- /src/types/CountryCode.ts: -------------------------------------------------------------------------------- 1 | export interface CountryCode { 2 | value: number; 3 | label: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/types/Attachment.ts: -------------------------------------------------------------------------------- 1 | export type Attachment = Pick & { 2 | url: string; 3 | }; 4 | -------------------------------------------------------------------------------- /tests/wa-js.test.ts-snapshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/tests/wa-js.test.ts-snapshots/demo.gif -------------------------------------------------------------------------------- /tests/wa-js.test.ts-snapshots/demo.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/tests/wa-js.test.ts-snapshots/demo.mp3 -------------------------------------------------------------------------------- /tests/wa-js.test.ts-snapshots/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/tests/wa-js.test.ts-snapshots/sample.mp4 -------------------------------------------------------------------------------- /tests/wa-js.test.ts-snapshots/sample.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcosvrs/WTF/HEAD/tests/wa-js.test.ts-snapshots/sample.pdf -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/types/Log.ts: -------------------------------------------------------------------------------- 1 | export default interface Log { 2 | level: number; 3 | message: string; 4 | attachment: boolean; 5 | contact: string; 6 | date?: string; 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("tailwindcss"), 4 | require("autoprefixer"), 5 | ...(process.env.NODE_ENV === "production" ? [require("cssnano")()] : []), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/types/ChromeMessageTypes.ts: -------------------------------------------------------------------------------- 1 | export enum ChromeMessageTypes { 2 | QUEUE_STATUS = "QUEUE_STATUS", 3 | SEND_MESSAGE = "SEND_MESSAGE", 4 | PAUSE_QUEUE = "PAUSE_QUEUE", 5 | RESUME_QUEUE = "RESUME_QUEUE", 6 | STOP_QUEUE = "STOP_QUEUE", 7 | ADD_LOG = "ADD_LOG", 8 | } 9 | -------------------------------------------------------------------------------- /src/types/Message.ts: -------------------------------------------------------------------------------- 1 | import type { MessageButtonsTypes } from "@wppconnect/wa-js/dist/chat/functions/prepareMessageButtons"; 2 | import type { Attachment } from "./Attachment"; 3 | 4 | export interface Message { 5 | contact: string; 6 | message: string; 7 | attachment?: Attachment; 8 | buttons: MessageButtonsTypes[]; 9 | delay?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/QueueStatus.ts: -------------------------------------------------------------------------------- 1 | export default interface QueueStatus { 2 | isProcessing: boolean; 3 | totalItems: number; 4 | processedItems: number; 5 | remainingItems: number; 6 | elapsedTime: number; 7 | processing: number | false; 8 | waiting: number | false; 9 | items: { 10 | detail: unknown; 11 | elapsedTime: number; 12 | }[]; 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | # Maintain dependencies for npm 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | allow: 15 | - dependency-name: "@wppconnect/wa-js" 16 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @keyframes progressBar { 6 | 0% { 7 | background-position: 0% 50%; 8 | } 9 | 50% { 10 | background-position: 100% 50%; 11 | } 12 | 100% { 13 | background-position: 0% 50%; 14 | } 15 | } 16 | 17 | .progress-bar { 18 | background: linear-gradient(270deg, #1e3a8a, #3b82f6, #9333ea); 19 | background-size: 200% 200%; 20 | } 21 | 22 | .progress-bar-animated { 23 | animation: progressBar 2s ease infinite; 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.formatOnSave": true 4 | }, 5 | "[typescript]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "eslint.validate": [ 9 | "javascript", 10 | "javascriptreact", 11 | "typescript", 12 | "typescriptreact" 13 | ], 14 | "files.autoSave": "onFocusChange", 15 | "search.exclude": { 16 | "**/.yarn": true, 17 | "**/.pnp.*": true 18 | }, 19 | "eslint.nodePath": ".yarn/sdks", 20 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", 21 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 22 | "typescript.enablePromptUseWorkspaceTsdk": true 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Automatically merge dependabot pull requests 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | automerge: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - id: automerge 13 | name: automerge 14 | uses: "pascalgn/automerge-action@v0.16.3" 15 | permissions: 16 | contents: write 17 | env: 18 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 19 | MERGE_FILTER_AUTHOR: dependabot[bot] 20 | MERGE_LABELS: dependencies 21 | MERGE_METHOD: rebase 22 | MERGE_FORKS: false 23 | MERGE_DELETE_BRANCH: true 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | boxShadow: { 7 | "equal-sm": "0 0 2px 0 rgb(0 0 0 / 0.05)", 8 | equal: "0 0 3px 0 rgb(0 0 0 / 0.1)", 9 | "equal-md": "0 0 6px -1px rgb(0 0 0 / 0.1)", 10 | "equal-lg": "0 0 15px -3px rgb(0 0 0 / 0.1)", 11 | "equal-xl": "0 0 25px -5px rgb(0 0 0 / 0.1)", 12 | "equal-2xl": "0 0 50px -12px rgb(0 0 0 / 0.25)", 13 | }, 14 | }, 15 | }, 16 | variants: { 17 | extend: { 18 | boxShadow: ["hover", "focus"], 19 | }, 20 | }, 21 | plugins: [], 22 | }; 23 | -------------------------------------------------------------------------------- /src/content_script.ts: -------------------------------------------------------------------------------- 1 | import { ChromeMessageTypes } from "types/ChromeMessageTypes"; 2 | import type Log from "types/Log"; 3 | import AsyncChromeMessageManager from "utils/AsyncChromeMessageManager"; 4 | 5 | const ContentScriptMessageManager = new AsyncChromeMessageManager( 6 | "contentScript", 7 | ); 8 | 9 | function addLog({ level, message, attachment = false, contact }: Log) { 10 | chrome.storage.local.get(({ logs = [] }: { logs: Log[] }) => { 11 | logs.push({ 12 | level, 13 | message, 14 | attachment, 15 | contact, 16 | date: new Date().toLocaleString(), 17 | }); 18 | void chrome.storage.local.set({ logs }); 19 | }); 20 | } 21 | 22 | ContentScriptMessageManager.addHandler(ChromeMessageTypes.ADD_LOG, (log) => { 23 | try { 24 | addLog(log); 25 | return true; 26 | } catch (error) { 27 | return false; 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /src/types/ChromeMessageContentTypes.ts: -------------------------------------------------------------------------------- 1 | import type { ChromeMessageTypes } from "./ChromeMessageTypes"; 2 | import type Log from "./Log"; 3 | import type { Message } from "./Message"; 4 | import type QueueStatus from "./QueueStatus"; 5 | 6 | export default interface ChromeMessageContentTypes { 7 | [ChromeMessageTypes.QUEUE_STATUS]: { 8 | payload: undefined; 9 | response: QueueStatus; 10 | }; 11 | [ChromeMessageTypes.SEND_MESSAGE]: { 12 | payload: Message; 13 | response: boolean; 14 | }; 15 | [ChromeMessageTypes.ADD_LOG]: { 16 | payload: Log; 17 | response: boolean; 18 | }; 19 | [ChromeMessageTypes.PAUSE_QUEUE]: { 20 | payload: undefined; 21 | response: boolean; 22 | }; 23 | [ChromeMessageTypes.RESUME_QUEUE]: { 24 | payload: undefined; 25 | response: boolean; 26 | }; 27 | [ChromeMessageTypes.STOP_QUEUE]: { 28 | payload: undefined; 29 | response: boolean; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | ## Is your feature request related to a problem? Please describe. 12 | 13 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 14 | 15 | ## Describe the solution you'd like 16 | 17 | A clear and concise description of what you want to happen. 18 | 19 | ## Describe alternatives you've considered 20 | 21 | A brief description of any alternative solutions or features you've considered. 22 | 23 | ## Additional context 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "index.css"; 4 | import LogTable from "components/organisms/LogTable"; 5 | import MessageButtonsForm from "components/organisms/MessageButtonsForm"; 6 | import MessageForm from "components/organisms/MessageForm"; 7 | 8 | class Options extends Component { 9 | override componentDidMount() { 10 | const body = document.querySelector("body"); 11 | if (!body) return; 12 | body.classList.add("bg-gray-100"); 13 | body.classList.add("dark:bg-gray-900"); 14 | body.style.minWidth = "48rem"; 15 | } 16 | 17 | override render() { 18 | return ( 19 | <> 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | 28 | createRoot(document.getElementById("root") ?? document.body).render( 29 | , 30 | ); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-present Marcos V. Rubido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | ## Describe the bug 12 | 13 | A clear and concise description of what the bug is. 14 | 15 | ## To Reproduce 16 | 17 | Steps to reproduce the behavior: 18 | 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | ## Expected behavior 25 | 26 | A clear and concise description of what you expected to happen. 27 | 28 | ## Screenshots 29 | 30 | If applicable, add screenshots to help explain your problem. 31 | 32 | ## Environment (please complete the following information): 33 | 34 | - OS: [e.g. iOS] 35 | - Browser [e.g. chrome, safari] 36 | - Version [e.g. 22] 37 | 38 | ## Console Output 39 | 40 | If there are any relevant console messages or logs, please provide them here. 41 | 42 | ## Additional context 43 | 44 | Add any other context about the problem here. 45 | -------------------------------------------------------------------------------- /tests/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { test as base, chromium } from "@playwright/test"; 2 | import { tmpdir } from "os"; 3 | import { join } from "path"; 4 | 5 | export const test = base.extend({ 6 | context: async ({ browserName }, use) => { 7 | const pathToExtension = join(__dirname, "../dist"); 8 | let userDataDir = join(tmpdir(), `wa-js-test-${browserName}`); 9 | if (process.env.WORKSPACE) { 10 | userDataDir = join(process.env.WORKSPACE, `wa-js-test-${browserName}`); 11 | } 12 | const browserArgs = [ 13 | `--disable-extensions-except=${pathToExtension}`, 14 | `--load-extension=${pathToExtension}`, 15 | ]; 16 | if (process.env.CI) { 17 | browserArgs.push("--disable-gpu", "--headless=new"); 18 | } 19 | const context = await chromium.launchPersistentContext(userDataDir, { 20 | args: browserArgs, 21 | }); 22 | await use(context); 23 | await context.close(); 24 | }, 25 | extensionId: async ({ context }, use) => { 26 | let [background] = context.serviceWorkers(); 27 | if (!background) background = await context.waitForEvent("serviceworker"); 28 | 29 | const extensionId = background.url().split("/")[2]; 30 | await use(extensionId); 31 | }, 32 | }); 33 | export const expect = test.expect; 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const CopyPlugin = require("copy-webpack-plugin"); 5 | 6 | /** @type {import('webpack').Configuration} */ 7 | module.exports = { 8 | context: path.resolve(__dirname, "src"), 9 | devtool: false, 10 | entry: fs 11 | .readdirSync(path.resolve(__dirname, "src")) 12 | .filter((file) => file.match(/\.tsx?$/)) 13 | .reduce( 14 | (acc, file) => ({ 15 | ...acc, 16 | [path.parse(file).name]: "./" + file, 17 | }), 18 | {}, 19 | ), 20 | output: { 21 | filename: "[name].js", 22 | path: path.resolve(__dirname, "dist", "js"), 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.([cm]?ts|tsx)$/, 28 | use: { 29 | loader: "ts-loader", 30 | options: { 31 | transpileOnly: true, 32 | }, 33 | }, 34 | exclude: /node_modules/, 35 | }, 36 | { 37 | test: /\.css$/, 38 | use: ["style-loader", "css-loader", "postcss-loader"], 39 | }, 40 | ], 41 | }, 42 | resolve: { 43 | extensions: [".ts", ".tsx", ".js"], 44 | extensionAlias: { 45 | ".ts": [".js", ".ts"], 46 | ".cts": [".cjs", ".cts"], 47 | ".mts": [".mjs", ".mts"], 48 | }, 49 | preferRelative: true, 50 | }, 51 | watchOptions: { 52 | ignored: ["/node_modules", "/dist"], 53 | poll: true, 54 | }, 55 | plugins: [ 56 | new CopyPlugin({ 57 | patterns: [{ from: ".", to: "../", context: "../public/" }], 58 | }), 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/atoms/ControlFactory.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | type InputHTMLAttributes, 4 | type SelectHTMLAttributes, 5 | type TextareaHTMLAttributes, 6 | } from "react"; 7 | 8 | const classNames = [ 9 | "w-full", 10 | "flex-auto", 11 | "bg-slate-100", 12 | "dark:bg-slate-900", 13 | "border", 14 | "border-slate-400", 15 | "dark:border-slate-600", 16 | "p-1", 17 | "rounded-lg", 18 | "transition-shadow", 19 | "ease-in-out", 20 | "duration-150", 21 | "focus:shadow-equal", 22 | "focus:shadow-blue-800", 23 | "dark:focus:shadow-blue-200", 24 | "focus:outline-none", 25 | ]; 26 | 27 | export class ControlInput extends Component< 28 | InputHTMLAttributes 29 | > { 30 | override render() { 31 | const { className = "" } = this.props; 32 | 33 | return ( 34 | 38 | ); 39 | } 40 | } 41 | 42 | export class ControlTextArea extends Component< 43 | TextareaHTMLAttributes 44 | > { 45 | override render() { 46 | const { className = "" } = this.props; 47 | return ( 48 |