├── img └── screenshot.png ├── public ├── icon128.png ├── icon16.png ├── icon48.png ├── popup.html └── vcbf.css ├── src ├── popup │ ├── logo.png │ ├── png.d.ts │ ├── popup.tsx │ ├── App.tsx │ └── App.css ├── background │ └── background.ts └── content │ └── content.ts ├── designs └── icon.sketch ├── .babelrc ├── .gitignore ├── tsconfig.json ├── scripts ├── archive.js ├── publish-firefox.js └── publish-chrome.js ├── manifests ├── webkit │ └── manifest.json └── firefox │ └── manifest.json ├── LICENSE ├── package.json ├── webpack ├── webpack.config.firefox.js └── webpack.config.webkit.js └── README.md /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Borodutch/bot-finder/HEAD/img/screenshot.png -------------------------------------------------------------------------------- /public/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Borodutch/bot-finder/HEAD/public/icon128.png -------------------------------------------------------------------------------- /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Borodutch/bot-finder/HEAD/public/icon16.png -------------------------------------------------------------------------------- /public/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Borodutch/bot-finder/HEAD/public/icon48.png -------------------------------------------------------------------------------- /src/popup/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Borodutch/bot-finder/HEAD/src/popup/logo.png -------------------------------------------------------------------------------- /designs/icon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Borodutch/bot-finder/HEAD/designs/icon.sketch -------------------------------------------------------------------------------- /src/popup/png.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | VC bots finder 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- / .babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/preset-react" 10 | ], 11 | "plugins": [ 12 | "react-hot-loader/babel" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/popup/popup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import App from "./App"; 5 | 6 | chrome.storage.local.get(['bots'], function (data) { 7 | ReactDOM.render( 8 | , 9 | document.getElementById("popup") 10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .env -------------------------------------------------------------------------------- /src/background/background.ts: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(function (details) { 2 | chrome.storage.local.set({ bots: true }); 3 | }); 4 | 5 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 6 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 7 | if (tabs[0].id) { 8 | chrome.tabs.sendMessage(tabs[0].id, { action: 'RELOAD' }, function (_) { 9 | void chrome.runtime.lastError; 10 | }); 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noImplicitAny": true, 8 | "module": "es6", 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "noImplicitThis": true, 12 | "strictNullChecks": true, 13 | "target": "es5", 14 | "allowJs": true, 15 | "jsx": "react", 16 | }, 17 | "include": [ 18 | "./src/**/*", 19 | "src/png.d.ts" 20 | ] 21 | } -------------------------------------------------------------------------------- /scripts/archive.js: -------------------------------------------------------------------------------- 1 | const { ZipAFolder } = require('zip-a-folder'); 2 | const fs = require('fs'); 3 | 4 | const archiveDir = `${__dirname}/../build`; 5 | const archivePath = `${archiveDir}/${new Date().toISOString()}.zip`; 6 | 7 | async function release() { 8 | if (!fs.existsSync(archiveDir)) { 9 | fs.mkdirSync(archiveDir); 10 | } 11 | console.log('Archiving started...'); 12 | try { 13 | await ZipAFolder.zip(`${__dirname}/../dist`, archivePath); 14 | console.log('Archiving finished.'); 15 | } catch (error) { 16 | console.log('Error happened: ' + error); 17 | } 18 | } 19 | 20 | release(); 21 | -------------------------------------------------------------------------------- /src/popup/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import logo from "./logo.png"; 3 | import "./App.css"; 4 | 5 | interface IAppProps { 6 | bots?: boolean 7 | } 8 | 9 | const App = (props: IAppProps) => { 10 | const [bots, setBots] = React.useState(props.bots); 11 | 12 | React.useEffect(() => { 13 | chrome.storage.local.set({bots: bots}); 14 | }, [bots]) 15 | 16 | return ( 17 |
18 |
19 | logo 20 |
21 | 22 | {setBots(!bots)}} /> 23 |
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /scripts/publish-firefox.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { zip } = require('zip-a-folder') 3 | 4 | async function release() { 5 | const timestamp = Date.now() 6 | console.log('Archiving the Firefox add-on...') 7 | await zip( 8 | `${__dirname}/../dist`, 9 | `/Users/nikitakolmogorov/Desktop/firefox.zip` 10 | ) 11 | console.log(`Moving node_modules to /tmp/${timestamp}...`) 12 | fs.mkdirSync(`/tmp/${timestamp}`) 13 | fs.renameSync( 14 | `${__dirname}/../node_modules`, 15 | `/tmp/${timestamp}/node_modules` 16 | ) 17 | console.log(`Archiving the Firefox sources...`) 18 | await zip( 19 | `${__dirname}/..`, 20 | `/Users/nikitakolmogorov/Desktop/firefox-sources.zip` 21 | ) 22 | console.log(`Moving node_modules back...`) 23 | fs.renameSync( 24 | `/tmp/${timestamp}/node_modules`, 25 | `${__dirname}/../node_modules` 26 | ) 27 | console.log( 28 | 'Add-on is archived, submit it at https://addons.mozilla.org/en-US/developers/addon/bot-finder/versions/submit/' 29 | ) 30 | } 31 | 32 | release() 33 | -------------------------------------------------------------------------------- /src/popup/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 4 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 5 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 6 | background-color: #111; 7 | } 8 | 9 | .App { 10 | font-size: 14px; 11 | } 12 | 13 | .App-header > p { 14 | padding: 10px 0 0 0; 15 | margin: 0; 16 | width: 100%; 17 | text-align: left; 18 | } 19 | 20 | .App-logo { 21 | pointer-events: none; 22 | width: 150px; 23 | } 24 | 25 | .App-header { 26 | background-color: #111; 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | justify-content: center; 31 | color: #fff; 32 | min-width: 100px; 33 | padding: 10px; 34 | } 35 | 36 | .App-settings { 37 | padding-top: 10px; 38 | width: 100%; 39 | display: flex; 40 | flex-direction: row; 41 | align-items: center; 42 | justify-content: space-between; 43 | } 44 | 45 | .App-link { 46 | color: #61dafb; 47 | } 48 | -------------------------------------------------------------------------------- /manifests/webkit/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Это бот?", 3 | "description": "Простое расширение, которое позволяет быстро увидеть, бот ли пользователь в комментариях.", 4 | "manifest_version": 2, 5 | "version": "1.1.4", 6 | "icons": { 7 | "16": "icon16.png", 8 | "48": "icon48.png", 9 | "128": "icon128.png" 10 | }, 11 | "browser_action": { 12 | "default_icon": { 13 | "16": "icon16.png", 14 | "48": "icon48.png" 15 | }, 16 | "default_popup": "popup.html" 17 | }, 18 | "permissions": [ 19 | "storage", 20 | "tabs", 21 | "activeTab" 22 | ], 23 | "background": { 24 | "scripts": [ 25 | "background.js" 26 | ], 27 | "persistent": true 28 | }, 29 | "short_name": "VC bots", 30 | "content_scripts": [ 31 | { 32 | "matches": [ 33 | "*://vc.ru/*", 34 | "*://tjournal.ru/*", 35 | "*://dtf.ru/*" 36 | ], 37 | "js": [ 38 | "content.js" 39 | ], 40 | "css": [ 41 | "vcbf.css" 42 | ], 43 | "run_at": "document_idle" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Stanislav Nepomniashchikh 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. -------------------------------------------------------------------------------- /manifests/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Это бот?", 3 | "description": "Простое расширение, которое позволяет быстро увидеть, бот ли пользователь в комментариях.", 4 | "manifest_version": 2, 5 | "version": "1.1.4", 6 | "icons": { 7 | "16": "icon16.png", 8 | "48": "icon48.png", 9 | "128": "icon128.png" 10 | }, 11 | "browser_action": { 12 | "default_icon": { 13 | "16": "icon16.png", 14 | "48": "icon48.png" 15 | }, 16 | "default_popup": "popup.html" 17 | }, 18 | "applications": { 19 | "gecko": { 20 | "id": "me@stasn.ru", 21 | "strict_min_version": "45.0" 22 | } 23 | }, 24 | "permissions": [ 25 | "storage", 26 | "tabs", 27 | "activeTab" 28 | ], 29 | "background": { 30 | "scripts": [ 31 | "background.js" 32 | ], 33 | "persistent": true 34 | }, 35 | "short_name": "VC bots", 36 | "content_scripts": [ 37 | { 38 | "matches": [ 39 | "*://vc.ru/*", 40 | "*://tjournal.ru/*", 41 | "*://dtf.ru/*" 42 | ], 43 | "js": [ 44 | "content.js" 45 | ], 46 | "css": [ 47 | "vcbf.css" 48 | ], 49 | "run_at": "document_idle" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /scripts/publish-chrome.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv') 2 | dotenv.config({ path: `${__dirname}/../.env` }) 3 | const fs = require('fs') 4 | const { zip } = require('zip-a-folder') 5 | 6 | const archivePath = `${__dirname}/../chrome.zip` 7 | 8 | async function release() { 9 | console.log('Archiving...') 10 | await zip(`${__dirname}/../dist`, archivePath) 11 | const webStore = require('chrome-webstore-upload')({ 12 | extensionId: 'fbjbccjcmmnegakmjkklplmijeilnbhd', 13 | clientId: process.env.GOOGLE_ID, 14 | clientSecret: process.env.GOOGLE_SECRET, 15 | refreshToken: process.env.GOOGLE_REFRESH, 16 | }) 17 | console.log('Getting an access token...') 18 | const token = await webStore.fetchToken() 19 | console.log('Uploading the archive...') 20 | const archive = fs.createReadStream(archivePath) 21 | const response = await webStore.uploadExisting(archive, token) 22 | if (response.itemError && response.itemError.length) { 23 | console.log('Error', response.itemError) 24 | process.exit(1) 25 | } 26 | console.log('Deleting the archive...') 27 | fs.unlinkSync(archivePath) 28 | console.log('Publishing the extension...') 29 | const status = (await webStore.publish('default', token)).status 30 | console.log('Finished with status', status) 31 | } 32 | 33 | release() 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bot-finder", 3 | "version": "1.0.4", 4 | "description": "Web extension that shows account creation date and last comments for users of vc.ru, dtf.ru and tjournal.ru in the comments.", 5 | "scripts": { 6 | "increment-version": "versiony package.json --patch && versiony manifests/firefox/manifest.json --patch && versiony manifests/webkit/manifest.json --patch && git add . && git commit -m 'new version' && git push", 7 | "build-webkit": "webpack --mode=production --config webpack/webpack.config.webkit.js", 8 | "build-firefox": "webpack --mode=production --config webpack/webpack.config.firefox.js", 9 | "archive": "node scripts/archive.js", 10 | "publish-chrome": "node scripts/publish-chrome.js", 11 | "publish-firefox": "node scripts/publish-firefox.js", 12 | "build-and-publish": "yarn increment-version && yarn build-webkit && yarn publish-chrome && yarn build-firefox && yarn publish-firefox" 13 | }, 14 | "keywords": [ 15 | "chrome", 16 | "opera", 17 | "firefox", 18 | "safari", 19 | "extension", 20 | "vc.ru", 21 | "vc bots finder" 22 | ], 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@babel/core": "^7.12.3", 26 | "@babel/preset-env": "^7.12.1", 27 | "@babel/preset-react": "^7.12.1", 28 | "@hot-loader/react-dom": "^17.0.0-rc.2", 29 | "@types/chrome": "0.0.125", 30 | "@types/react": "^16.9.53", 31 | "@types/react-dom": "^16.9.8", 32 | "babel-loader": "^8.1.0", 33 | "chrome-webstore-upload": "^0.4.4", 34 | "copy-webpack-plugin": "^6.2.1", 35 | "css-loader": "^5.0.0", 36 | "dotenv": "^8.2.0", 37 | "file-loader": "^6.1.1", 38 | "style-loader": "^2.0.0", 39 | "ts-loader": "^8.0.5", 40 | "typescript": "^4.0.3", 41 | "url-loader": "^4.1.1", 42 | "versiony": "^2.0.1", 43 | "webpack": "^5.1.3", 44 | "webpack-cli": "^4.0.0", 45 | "webpack-dev-server": "^3.11.0", 46 | "zip-a-folder": "^0.0.12" 47 | }, 48 | "dependencies": { 49 | "react": "^16.14.0", 50 | "react-dom": "^16.14.0", 51 | "react-hot-loader": "^4.13.0" 52 | } 53 | } -------------------------------------------------------------------------------- /webpack/webpack.config.firefox.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | 5 | const config = { 6 | entry: { 7 | popup: path.join(__dirname, '../src/popup/popup.tsx'), 8 | content: path.join(__dirname, '../src/content/content.ts'), 9 | background: path.join(__dirname, '../src/background/background.ts'), 10 | }, 11 | output: { 12 | path: path.join(__dirname, '../dist'), 13 | filename: '[name].js', 14 | clean: true, 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.(js|jsx)$/, 20 | use: 'babel-loader', 21 | exclude: /node_modules/, 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: ['style-loader', 'css-loader'], 26 | exclude: /\.module\.css$/, 27 | }, 28 | { 29 | test: /\.ts(x)?$/, 30 | loader: 'ts-loader', 31 | exclude: /node_modules/, 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | 'style-loader', 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | importLoaders: 1, 41 | modules: true, 42 | }, 43 | }, 44 | ], 45 | include: /\.module\.css$/, 46 | }, 47 | { 48 | test: /\.svg$/, 49 | use: 'file-loader', 50 | }, 51 | { 52 | test: /\.png$/, 53 | use: [ 54 | { 55 | loader: 'url-loader', 56 | options: { 57 | mimetype: 'image/png', 58 | }, 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | resolve: { 65 | extensions: ['.js', '.jsx', '.tsx', '.ts'], 66 | alias: { 67 | 'react-dom': '@hot-loader/react-dom', 68 | }, 69 | }, 70 | devServer: { 71 | contentBase: './dist', 72 | }, 73 | plugins: [ 74 | new CopyPlugin({ 75 | patterns: [ 76 | { from: 'public', to: '.' }, 77 | { from: 'manifests/firefox/manifest.json', to: '.' }, 78 | ], 79 | }), 80 | ], 81 | } 82 | 83 | module.exports = config 84 | -------------------------------------------------------------------------------- /webpack/webpack.config.webkit.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | 5 | const config = { 6 | entry: { 7 | popup: path.join(__dirname, '../src/popup/popup.tsx'), 8 | content: path.join(__dirname, '../src/content/content.ts'), 9 | background: path.join(__dirname, '../src/background/background.ts'), 10 | }, 11 | output: { 12 | path: path.join(__dirname, '../dist'), 13 | filename: '[name].js', 14 | clean: true, 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.(js|jsx)$/, 20 | use: 'babel-loader', 21 | exclude: /node_modules/, 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: ['style-loader', 'css-loader'], 26 | exclude: /\.module\.css$/, 27 | }, 28 | { 29 | test: /\.ts(x)?$/, 30 | loader: 'ts-loader', 31 | exclude: /node_modules/, 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: [ 36 | 'style-loader', 37 | { 38 | loader: 'css-loader', 39 | options: { 40 | importLoaders: 1, 41 | modules: true, 42 | }, 43 | }, 44 | ], 45 | include: /\.module\.css$/, 46 | }, 47 | { 48 | test: /\.svg$/, 49 | use: 'file-loader', 50 | }, 51 | { 52 | test: /\.png$/, 53 | use: [ 54 | { 55 | loader: 'url-loader', 56 | options: { 57 | mimetype: 'image/png', 58 | }, 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | resolve: { 65 | extensions: ['.js', '.jsx', '.tsx', '.ts'], 66 | alias: { 67 | 'react-dom': '@hot-loader/react-dom', 68 | }, 69 | }, 70 | devServer: { 71 | contentBase: './dist', 72 | }, 73 | plugins: [ 74 | new CopyPlugin({ 75 | patterns: [ 76 | { from: 'public', to: '.' }, 77 | { from: 'manifests/webkit/manifest.json', to: '.' }, 78 | ], 79 | }), 80 | ], 81 | } 82 | 83 | module.exports = config 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Bot finder

2 | 3 |

4 | Screenshot 5 |

6 | 7 |

8 | Web extension that shows account creation date and other info for users of vc.ru, TJ and DTF in the comments. 9 |

10 |

11 | 12 | WebsiteChrome Web StoreFirefox Browser Add-ons 13 | 14 |

15 | 16 | ## Getting started 17 | 18 | Navigate to the project directory and install the dependencies. 19 | 20 | ``` 21 | $ yarn 22 | ``` 23 | 24 | ### To build the extension for Chrome and Opera, run 25 | 26 | ``` 27 | $ yarn build-webkit && yarn archive 28 | ``` 29 | 30 | ### To build the extension for Safari 31 | 32 | You need to have XCode installed on your Mac. 33 | 34 | Follow instructions for [Chrome](https://github.com/backmeupplz/bot-finder/#to-build-the-extension-for-chrome-and-opera-run), then follow these instructions from Apple: 35 | 36 | https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari. The command you need to use is probably going to look like this: 37 | 38 | ``` 39 | xcrun safari-web-extension-converter /Volumes/Experiments/bot-finder/dist/ 40 | ``` 41 | 42 | If you don't have developer certificate, you'll also need to follow these instructions: 43 | https://developer.apple.com/documentation/safariservices/safari_app_extensions/building_a_safari_app_extension#2957926 (see "Enable Your App Extension in Safari"). 44 | 45 | ### To build the extension for Firefox, run 46 | 47 | ``` 48 | $ yarn build-firefox && yarn archive 49 | ``` 50 | 51 | After the project has been built, a directories named `dist` and `build` (with build archive) have been created. 52 | 53 | ## Publications 54 | - [vc.ru](https://vc.ru/tribuna/226020-eto-bot-rasshirenie-dlya-brauzera-pokazyvayushchee-bolshe-informacii-o-kommentatorah-na-vc-ru) 55 | - [TJ](https://tjournal.ru/flood/362516-my-napisali-rasshirenie-dlya-brauzera-pokazyvayushchee-bolshe-informacii-o-kommentatorah-na-tjournal) 56 | - [DTF](https://dtf.ru/flood/699403-my-napisali-rasshirenie-dlya-brauzera-pokazyvayushchee-bolshe-informacii-o-kommentatorah-na-dtf) 57 | -------------------------------------------------------------------------------- /public/vcbf.css: -------------------------------------------------------------------------------- 1 | .__vcbf { 2 | position: relative; 3 | -webkit-user-select: none; 4 | -moz-user-select: none; 5 | -ms-user-select: none; 6 | user-select: none; 7 | } 8 | 9 | .__vcbf__popup { 10 | position: absolute; 11 | right: 0; 12 | top: 27px; 13 | color: #000; 14 | background-color: #fff; 15 | -webkit-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.07), 16 | 0 4px 8px 0 rgba(0, 0, 0, 0.07); 17 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.07), 0 4px 8px 0 rgba(0, 0, 0, 0.07); 18 | text-align: left; 19 | font-size: 13px; 20 | border-radius: 5px; 21 | z-index: 6; 22 | min-width: 100px; 23 | } 24 | 25 | .__vcbf__popup:before { 26 | content: ''; 27 | position: absolute; 28 | width: 10px; 29 | height: 10px; 30 | background: #fff; 31 | -webkit-transform: rotate(45deg); 32 | transform: rotate(45deg); 33 | top: -5px; 34 | right: 23px; 35 | -webkit-box-shadow: -1px -1px 0 0 rgba(0, 0, 0, 0.07); 36 | box-shadow: -1px -1px 0 0 rgba(0, 0, 0, 0.07); 37 | } 38 | 39 | .__vcbf__popup-preloader { 40 | padding: 10px; 41 | color: #595959; 42 | } 43 | 44 | .__vcbf__popup-header { 45 | display: flex; 46 | justify-content: space-between; 47 | align-items: center; 48 | padding: 10px; 49 | border-bottom: 1px solid #f8f8f8; 50 | line-height: 1.3; 51 | } 52 | 53 | .__vcbf__popup-header_name { 54 | font-size: 14px; 55 | overflow: hidden; 56 | font-weight: 700; 57 | text-overflow: ellipsis; 58 | white-space: nowrap; 59 | } 60 | 61 | .__vcbf__popup-header_karma { 62 | margin-left: 10px; 63 | text-align: center; 64 | padding: 2px 6px 1px 6px; 65 | min-width: 24px; 66 | color: #595959; 67 | background-color: #eee; 68 | border-radius: 5px; 69 | } 70 | 71 | .__vcbf__popup-header_karma--positive { 72 | color: #0a1; 73 | background-color: #eefbf3; 74 | } 75 | 76 | .__vcbf__popup-header_karma--negative { 77 | color: #cd192e; 78 | background-color: rgba(89,89,89,.1); 79 | } 80 | 81 | .__vcbf__popup-content { 82 | padding: 10px; 83 | margin-bottom: -5px; 84 | } 85 | 86 | .__vcbf__popup-content > div { 87 | white-space: nowrap; 88 | padding-bottom: 5px; 89 | } 90 | 91 | .__vcbf__popup-content_date { 92 | padding-bottom: 10px !important; 93 | } 94 | 95 | .__vcbf__popup-content > div > span { 96 | display: inline !important; 97 | margin: 0 !important; 98 | font-weight: 500; 99 | } 100 | 101 | @media only screen and (max-width: 600px) { 102 | .__vcbf__popup { 103 | max-width: 200px; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/content/content.ts: -------------------------------------------------------------------------------- 1 | const parseUser = async (url: string | null) => { 2 | if (!url) return null; 3 | let userInfo: NodeListOf | undefined; 4 | await fetch(url) 5 | .then((response) => response.text()) 6 | .then((html) => { 7 | let parser = new DOMParser(); 8 | let doc = parser.parseFromString(html, 'text/html'); 9 | userInfo = doc.querySelectorAll('textarea.l-hidden'); 10 | }); 11 | if (userInfo !== undefined) return JSON.parse(userInfo[0].innerHTML); 12 | return null; 13 | }; 14 | 15 | const runScript = async () => { 16 | const commentNames = Array.from( 17 | document.querySelectorAll('.comments__item__space') 18 | ); 19 | 20 | for (let username of commentNames) { 21 | const userDom = username.getElementsByClassName('comments__item__user')[0]; 22 | const userLink = userDom ? userDom.getAttribute('href') : null; 23 | 24 | if (userLink) { 25 | const botButton = document.createElement('div'); 26 | const botDropdown = document.createElement('div'); 27 | botDropdown.className = '__vcbf__popup'; 28 | botDropdown.style.display = 'none'; 29 | botButton.className = 'comments__item__reply __vcbf'; 30 | botButton.innerHTML = 'Это бот?'; 31 | botButton.onclick = async () => { 32 | window.addEventListener('click', function (e) { 33 | if ( 34 | e.target && 35 | !(e.target).classList.contains('__vcbf') 36 | ) { 37 | if (botDropdown.style.display === 'block') { 38 | botDropdown.style.display = 'none'; 39 | } 40 | } 41 | }); 42 | 43 | if (botDropdown.style.display === 'none') { 44 | // Preloader 45 | let html = '
'; 46 | html += 'Загрузка...'; 47 | html += '
'; 48 | botDropdown.innerHTML = html; 49 | botDropdown.style.display = 'block'; 50 | // Load user data 51 | const userData = await parseUser(userLink); 52 | // Parse user data 53 | const user = { 54 | name: userData.header.subsiteData.name, 55 | karma: userData.header.subsiteData.karma || 0, 56 | registerDate: userData.header.stats[0].label, 57 | commentsCount: userData.header.tabs[1].counter, 58 | articlesCount: userData.header.tabs[0].counter, 59 | isPremium: userData.header.subsiteData.isPlus, 60 | }; 61 | // Styling karma 62 | let karmaClass = ' '; 63 | if (user.karma > 0) { 64 | karmaClass += '__vcbf__popup-header_karma--positive'; 65 | user.karma = '+' + user.karma; 66 | } else if (user.karma < 0) { 67 | karmaClass += '__vcbf__popup-header_karma--negative'; 68 | } 69 | // Set popup header 70 | html = ''; 71 | html += '
'; 72 | html += `
${user.name}
`; 73 | html += `
${user.karma}
`; 74 | html += '
'; 75 | // Set popup content 76 | html += '
'; 77 | html += ``; 78 | html += `
Премиум: ${ 79 | user.isPremium ? 'да' : 'нет' 80 | }
`; 81 | html += `
Комментариев: ${user.commentsCount} шт.
`; 82 | html += `
Статей: ${user.articlesCount} шт.
`; 83 | html += '
'; 84 | 85 | botDropdown.innerHTML = html; 86 | } else { 87 | botDropdown.style.display = 'none'; 88 | } 89 | }; 90 | username 91 | .getElementsByClassName('comments__item__self')[0] 92 | .appendChild(botButton); 93 | username.getElementsByClassName('__vcbf')[0].appendChild(botDropdown); 94 | } 95 | } 96 | }; 97 | 98 | chrome.storage.local.get(['bots'], function (data) { 99 | if (data.bots) { 100 | chrome.runtime.onMessage.addListener(function ( 101 | request, 102 | sender, 103 | sendResponse 104 | ) { 105 | const botButtonsToRemove = Array.from( 106 | document.querySelectorAll('.__vcbf') 107 | ); 108 | for (let botButton of botButtonsToRemove) { 109 | botButton.remove(); 110 | } 111 | runScript(); 112 | }); 113 | } 114 | }); 115 | --------------------------------------------------------------------------------