diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8a42b78 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*] +end_of_line = lf +insert_final_newline = true + +[{*.js,*.ts,*.tsx,*.json,*.css}] +indent_style = space +indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..121f232 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + extends: ["standard-with-typescript", "standard-jsx", "prettier"], + parserOptions: { + project: "./tsconfig.json", + }, + rules: { + "@typescript-eslint/member-delimiter-style": 2, + "@typescript-eslint/semi": ["error", "always"], + "@typescript-eslint/no-extra-semi": "error", + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/strict-boolean-expressions": 0, + "@typescript-eslint/no-floating-promises": 0, + "@typescript-eslint/prefer-nullish-coalescing": 0, + "@typescript-eslint/restrict-template-expressions": 0, + "@typescript-eslint/promise-function-async": 0, + "@typescript-eslint/consistent-type-definitions": 0, + "@typescript-eslint/no-misused-promises": ["error", {"checksConditionals": false}], + "@typescript-eslint/no-redeclare": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/member-delimiter-style": 0, + "import/no-absolute-path": 0, + "no-return-assign": 0, + "no-useless-return": 0, + "quotes": 0, + "object-curly-spacing": 2, + "n/no-callback-literal": 0, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e2e84b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +node_modules/ diff --git a/.projectile b/.projectile new file mode 100644 index 0000000..c9543be --- /dev/null +++ b/.projectile @@ -0,0 +1,2 @@ +-node_modules/ +-build/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bbc196 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +## About + +Photon is a Google Chrome extension that provides information about +the public IP address you are using to connect to the internet. When +the extension's icon is clicked, the extension makes a request to the +[clean.wtfismyip.com](https://clean.wtfismyip.com) +web service to discover information about your public IP address, and +then displays that information in a simple HTML view. + +## Features + +* Displays a public IP address. +* Displays the Internet Service Provider (ISP) associated with a public IP address. +* Displays the city associated with a public IP address. +* Displays the country associated with a public IP address. +* Displays whether or not a public IP address is a Tor exit node. + +## Install + +Produce the "build" directory: + + git clone https://github.com/0x1eef/photon.git + cd photon + npm i + npm run build + +Add the extension to Chrome: + +* Visit `chrome://extensions`. +* Check "Developer mode" (top right hand corner). +* Click "Load unpacked extension". +* Choose the "build/" directory from the file dialog. +* Done. + +## Sources + +* [Source code (GitHub)](https://github.com/0x1eef/photon#readme) +* [Source code (GitLab)](https://gitlab.com/0x1eef/photon#about) + +## License + +[BSD Zero Clause](https://choosealicense.com/licenses/0bsd/). +
+See [LICENSE](./LICENSE) + + diff --git a/package-lock.json b/package-lock.json index af744ef..fbaf28d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@types/chrome": "^0.0.246", "@types/react": "^18.0.18", "@types/react-dom": "^18.0.6", "clean-webpack-plugin": "^4.0.0", @@ -224,6 +225,16 @@ "node": ">= 8" } }, + "node_modules/@types/chrome": { + "version": "0.0.246", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.246.tgz", + "integrity": "sha512-MxGxEomGxsJiL9xe/7ZwVgwdn8XVKWbPvxpVQl3nWOjrS0Ce63JsfzxUc4aU3GvRcUPYsfufHmJ17BFyKxeA4g==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -250,6 +261,21 @@ "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==", "dev": true }, + "node_modules/@types/filesystem": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.33.tgz", + "integrity": "sha512-2KedRPzwu2K528vFkoXnnWdsG0MtUwPjuA7pRy4vKxlxHEe8qUDZibYHXJKZZr2Cl/ELdCWYqyb/MKwsUuzBWw==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.30.tgz", + "integrity": "sha512-lB98tui0uxc7erbj0serZfJlHKLNJHwBltPnbmO1WRpL5T325GOHRiQfr2E29V2q+S1brDO63Fpdt6vb3bES9Q==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -260,6 +286,12 @@ "@types/node": "*" } }, + "node_modules/@types/har-format": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.13.tgz", + "integrity": "sha512-PwBsCBD3lDODn4xpje3Y1di0aDJp4Ww7aSfMRVw6ysnxD4I7Wmq2mBkSKaDtN403hqH5sp6c9xQUvFYY3+lkBg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", diff --git a/package.json b/package.json index e8909e6..e9e717e 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,12 @@ { - "name": "whatismyip", + "name": "photon", + "version": "0.1.0", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { + "@types/chrome": "^0.0.246", "@types/react": "^18.0.18", "@types/react-dom": "^18.0.6", "clean-webpack-plugin": "^4.0.0", diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json new file mode 100644 index 0000000..78f6ed8 --- /dev/null +++ b/src/_locales/en/messages.json @@ -0,0 +1,26 @@ +{ + "ip_address": { + "message": "IP Address" + }, + "isp": { + "message": "ISP" + }, + "city": { + "message": "City" + }, + "country": { + "message": "Country" + }, + "tor_exit_node": { + "message": "Tor exit node" + }, + "loading": { + "message": "Loading..." + }, + "yes": { + "message": "Yes" + }, + "no": { + "message": "No" + } +} diff --git a/src/css/components/Response.css b/src/css/components/Response.css new file mode 100644 index 0000000..7e57e24 --- /dev/null +++ b/src/css/components/Response.css @@ -0,0 +1,10 @@ +body .response { + padding: 30px; +} + +body .response .row { + display: flex; + place-content: space-between; + font-size: var(--default-font-size); + margin: 0 0 10px 0; +} diff --git a/src/css/index.css b/src/css/index.css new file mode 100644 index 0000000..6450af4 --- /dev/null +++ b/src/css/index.css @@ -0,0 +1,52 @@ +:root { + /* Defaults */ + --default-font-size: 1.15em; + --default-font-family: "SF Mono","Segoe UI Mono","Roboto Mono",Menlo,Courier,monospace; + + /* Colors */ + --primary-red: #d73e48; + --primary-green: #32b643; + --primary-blue: #302ecd; + --secondary-blue: #807fe2; + --primary-white: #FFF; + --secondary-white: #f1f1fc; +} + +body { + font-family: var(--default-font-family); + width: 368px; + height: 212px; + margin: 0; +} + +#root, .loader { + height: 100%; +} +a { + color: var(--primary-blue) !important; + text-decoration: none; +} +a:visited { + color: var(--primary-blue) !important; +} +a:active, a:focus, a:hover { + color: var(--primary-blue) !important; + text-decoration: underline; +} + +footer { + background: var(--secondary-white); + color: var(--primary-red); + position: fixed; + bottom: 0; + width: 100%; + padding: 10px; + text-align: center; +} + +.loader { + display: flex; + flex-direction: column; + align-items: center; + place-content: center; +} diff --git a/src/css/label.css b/src/css/label.css new file mode 100644 index 0000000..ae6c199 --- /dev/null +++ b/src/css/label.css @@ -0,0 +1,17 @@ +.label { + padding: 4px; +} + +.label-rounded { + border-radius: 5px; +} + +.label-error { + background: var(--primary-red); + color: var(--primary-white); +} + +.label-success { + background: var(--primary-green); + color: var(--primary-white); +} diff --git a/src/html/browseraction.html b/src/html/browseraction.html new file mode 100644 index 0000000..70c2e28 --- /dev/null +++ b/src/html/browseraction.html @@ -0,0 +1,15 @@ + + + + + + + + +
+ + + + diff --git a/src/images/icons/icon128.png b/src/images/icons/icon128.png new file mode 100644 index 0000000..ac0f978 Binary files /dev/null and b/src/images/icons/icon128.png differ diff --git a/src/images/icons/icon16.png b/src/images/icons/icon16.png new file mode 100644 index 0000000..a1425ee Binary files /dev/null and b/src/images/icons/icon16.png differ diff --git a/src/js/components/App.tsx b/src/js/components/App.tsx new file mode 100644 index 0000000..fff8d13 --- /dev/null +++ b/src/js/components/App.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { ResponseRenderer } from "/components/ResponseRenderer"; +import { useWebService } from "/hooks/useWebService"; + +export function App() { + const [response, error] = useWebService(); + const t = chrome.i18n.getMessage; + if (response) { + return (); + } else if (error) { + /* FIXME: Add ErrorRenderer */ + } else { + return (
{t("loading")}
); + } +} diff --git a/src/js/components/ResponseRenderer.tsx b/src/js/components/ResponseRenderer.tsx new file mode 100644 index 0000000..17fae8a --- /dev/null +++ b/src/js/components/ResponseRenderer.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { TResponse } from "/lib/response"; + +export function ResponseRenderer({ response }: {response: TResponse}) { + const t = chrome.i18n.getMessage; + + function YesOrNoLabel({yes}: {yes: boolean}) { + if (yes) { + return (); + } else { + return (); + } + } + + return ( +
+
+
{t("ip_address")}
+
{response.IPAddress}
+
+
+
{t("isp")}
+
{response.ISP}
+
+
+
{t("city")}
+
{response.City}
+
+
+
{t("country")}
+
{response.Country}
+
+
+
{t("tor_exit_node")}
+
{}
+
+
+ ); +} diff --git a/src/js/hooks/useWebService.ts b/src/js/hooks/useWebService.ts new file mode 100644 index 0000000..048440d --- /dev/null +++ b/src/js/hooks/useWebService.ts @@ -0,0 +1,19 @@ +import { TResponse, Response } from "/lib/response"; +import { useEffect, useState } from "react"; + +type Maybe = T | null; + +export function useWebService(): [Maybe, Maybe] { + const endpoint = "https://wtfismyip.com/json"; + const [response, setResponse] = useState>(null); + const [error, setError] = useState>(null); + + useEffect(() => { + fetch(endpoint) + .then((res) => res.json()) + .then((json) => setResponse(Response(json))) + .catch((err) => setError(err)); + }); + + return [response, error]; +} diff --git a/src/js/index.tsx b/src/js/index.tsx new file mode 100644 index 0000000..5ba1a1f --- /dev/null +++ b/src/js/index.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { App } from "/components/App"; + +document.addEventListener("DOMContentLoaded", () => { + const el: HTMLElement = document.getElementById("root")!; + ReactDOM.createRoot(el).render(); +}); diff --git a/src/js/lib/response.ts b/src/js/lib/response.ts new file mode 100644 index 0000000..a493f1e --- /dev/null +++ b/src/js/lib/response.ts @@ -0,0 +1,30 @@ +export type TResponse = { + IPAddress: string; + Location: string; + ISP: string; + City: string; + Country: string; + isTorExitNode: boolean; +}; + +export type TServerResponse = { + YourFuckingIPAddress: string; + YourFuckingLocation: string; + YourFuckingISP: string; + YourFuckingCity: string; + YourFuckingCountry: string; + YourFuckingTorExit: boolean; +}; + +export function Response(res: TServerResponse): TResponse { + const self = Object.create(null); + + self.IPAddress = res.YourFuckingIPAddress; + self.Location = res.YourFuckingLocation; + self.ISP = res.YourFuckingISP; + self.City = res.YourFuckingCity; + self.Country = res.YourFuckingCountry; + self.isTorExitNode = res.YourFuckingTorExit; + + return self; +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..ddeabc1 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,15 @@ +{ + "manifest_version": 3, + "name": "Photon", + "version": "0.1.0", + "description": "Provides various information about your public IP address.", + "action": { + "default_popup": "/html/browseraction.html" + }, + "permissions": [], + "icons": { + "16": "images/icons/icon16.png", + "128": "images/icons/icon128.png" + }, + "default_locale": "en" +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..15a5eaf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "strict": true, + "strictNullChecks": false, + "module": "commonjs", + "target": "ES2020", + "noImplicitAny": true, + "moduleResolution": "node", + "esModuleInterop": true, + "jsx": "react", + "allowJs": true, + "lib": [ "ES2020", "DOM" ], + + "baseUrl": "src/", + "paths": { "*": ["js/*"] }, + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..0682059 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,42 @@ +const path = require('path'); +const process = require('process'); +const CopyPlugin = require("copy-webpack-plugin"); +const { CleanWebpackPlugin } = require("clean-webpack-plugin"); + +module.exports = { + mode: process.env.NODE_ENV || "development", + devtool: "inline-source-map", + entry: { + index: './src/js/index.tsx', + }, + output: { + filename: 'js/[name].js', + path: path.resolve(__dirname, 'build') + }, + resolve: { + roots: [path.resolve('src/js'), path.resolve('node_modules')], + modules: [path.resolve('src/js'), path.resolve('node_modules')], + extensions: ['.ts', '.tsx'] + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { from: "src/html/", to: "html" }, + { from: "src/css/", to: "css" }, + { from: "src/manifest.json", to: "manifest.json" }, + { from: "src/images", to: "images/" }, + { from: "src/_locales", to: "_locales/" } + ], + }), + new CleanWebpackPlugin(), + ], +}