mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 18:12:25 -06:00 
			
		
		
		
	* 🐸restructure frontend stuff, include admin and future user panel in main repo, properly deduplicate bundles for css+js across uses
* rename bundled to dist, caught by gitignore
* re-include status.css for profile template
* default to localhost
* serve frontend panels
* add todo message for abstraction
* refactor oauth registration flow
* oauth restructure
* update footer template
* change panel routes
* remove superfluous css imports
* write bundle to disk from test server, use forked budo-express
* wrap all page content in container
for robustness with addons etc injection other elements in body
* update documentation, goreleaser, Dockerfile
* update template meta tags
* add AGPL-3.0+ license header everywhere
* only attach update listener on EventEmitter
* cleaner config for various frontend bundles
* fix bundler script paths
* Merge commit 'd191931932'
* fix up dockerfile, goreleaser
* go mod tidy
* add uglifyify
* move status hide/show js to frontend bundle
* fix stylesheet color( func regressions
* update contributing docs for new build path
* update goreleaser + docker building
* resolve dependency paths properly
* update package name
* use api errorhandler
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
		
	
			
		
			
				
	
	
		
			318 lines
		
	
	
		
			No EOL
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			318 lines
		
	
	
		
			No EOL
		
	
	
		
			8.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/*
 | 
						|
   GoToSocial
 | 
						|
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
 | 
						|
 | 
						|
   This program is free software: you can redistribute it and/or modify
 | 
						|
   it under the terms of the GNU Affero General Public License as published by
 | 
						|
   the Free Software Foundation, either version 3 of the License, or
 | 
						|
   (at your option) any later version.
 | 
						|
 | 
						|
   This program is distributed in the hope that it will be useful,
 | 
						|
   but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
   GNU Affero General Public License for more details.
 | 
						|
 | 
						|
   You should have received a copy of the GNU Affero General Public License
 | 
						|
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
						|
*/
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
const Promise = require("bluebird");
 | 
						|
const React = require("react");
 | 
						|
const fileDownload = require("js-file-download");
 | 
						|
 | 
						|
function sortBlocks(blocks) {
 | 
						|
	return blocks.sort((a, b) => { // alphabetical sort
 | 
						|
		return a.domain.localeCompare(b.domain);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
function deduplicateBlocks(blocks) {
 | 
						|
	let a = new Map();
 | 
						|
	blocks.forEach((block) => {
 | 
						|
		a.set(block.id, block);
 | 
						|
	});
 | 
						|
	return Array.from(a.values());
 | 
						|
}
 | 
						|
 | 
						|
module.exports = function Blocks({oauth}) {
 | 
						|
	const [blocks, setBlocks] = React.useState([]);
 | 
						|
	const [info, setInfo] = React.useState("Fetching blocks");
 | 
						|
	const [errorMsg, setError] = React.useState("");
 | 
						|
	const [checked, setChecked] = React.useState(new Set());
 | 
						|
 | 
						|
	React.useEffect(() => {
 | 
						|
		Promise.try(() => {
 | 
						|
			return oauth.apiRequest("/api/v1/admin/domain_blocks", undefined, undefined, "GET");
 | 
						|
		}).then((json) => {
 | 
						|
			setInfo("");
 | 
						|
			setError("");
 | 
						|
			setBlocks(sortBlocks(json));
 | 
						|
		}).catch((e) => {
 | 
						|
			setError(e.message);
 | 
						|
			setInfo("");
 | 
						|
		});
 | 
						|
	}, []);
 | 
						|
 | 
						|
	let blockList = blocks.map((block) => {
 | 
						|
		function update(e) {
 | 
						|
			let newChecked = new Set(checked.values());
 | 
						|
			if (e.target.checked) {
 | 
						|
				newChecked.add(block.id);
 | 
						|
			} else {
 | 
						|
				newChecked.delete(block.id);
 | 
						|
			}
 | 
						|
			setChecked(newChecked);
 | 
						|
		}
 | 
						|
 | 
						|
		return (
 | 
						|
			<React.Fragment key={block.id}>
 | 
						|
				<div><input type="checkbox" onChange={update} checked={checked.has(block.id)}></input></div>
 | 
						|
				<div>{block.domain}</div>
 | 
						|
				<div>{(new Date(block.created_at)).toLocaleString()}</div>
 | 
						|
			</React.Fragment>
 | 
						|
		);
 | 
						|
	});
 | 
						|
 | 
						|
	function clearChecked() {
 | 
						|
		setChecked(new Set());
 | 
						|
	}
 | 
						|
 | 
						|
	function undoChecked() {
 | 
						|
		let amount = checked.size;
 | 
						|
		if(confirm(`Are you sure you want to remove ${amount} block(s)?`)) {
 | 
						|
			setInfo("");
 | 
						|
			Promise.map(Array.from(checked.values()), (block) => {
 | 
						|
				console.log("deleting", block);
 | 
						|
				return oauth.apiRequest(`/api/v1/admin/domain_blocks/${block}`, "DELETE");
 | 
						|
			}).then((res) => {
 | 
						|
				console.log(res);
 | 
						|
				setInfo(`Deleted ${amount} blocks: ${res.map((a) => a.domain).join(", ")}`);
 | 
						|
			}).catch((e) => {
 | 
						|
				setError(e);
 | 
						|
			});
 | 
						|
 | 
						|
			let newBlocks = blocks.filter((block) => {
 | 
						|
				if (checked.size > 0 && checked.has(block.id)) {
 | 
						|
					checked.delete(block.id);
 | 
						|
					return false;
 | 
						|
				} else {
 | 
						|
					return true;
 | 
						|
				}
 | 
						|
			});
 | 
						|
			setBlocks(newBlocks);
 | 
						|
			clearChecked();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return (
 | 
						|
		<section className="blocks">
 | 
						|
			<h1>Blocks</h1>
 | 
						|
			<div className="error accent">{errorMsg}</div>
 | 
						|
			<div>{info}</div>
 | 
						|
			<AddBlock oauth={oauth} blocks={blocks} setBlocks={setBlocks} />
 | 
						|
			<h3>Blocks:</h3>
 | 
						|
			<div style={{display: "grid", gridTemplateColumns: "1fr auto"}}>
 | 
						|
				<span onClick={clearChecked} className="accent" style={{alignSelf: "end"}}>uncheck all</span>
 | 
						|
				<button onClick={undoChecked}>Unblock selected</button>
 | 
						|
			</div>
 | 
						|
			<div className="blocklist overflow">
 | 
						|
				{blockList}
 | 
						|
			</div>
 | 
						|
			<BulkBlocking oauth={oauth} blocks={blocks} setBlocks={setBlocks}/>
 | 
						|
		</section>
 | 
						|
	);
 | 
						|
};
 | 
						|
 | 
						|
function BulkBlocking({oauth, blocks, setBlocks}) {
 | 
						|
	const [bulk, setBulk] = React.useState("");
 | 
						|
	const [blockMap, setBlockMap] = React.useState(new Map());
 | 
						|
	const [output, setOutput] = React.useState();
 | 
						|
 | 
						|
	React.useEffect(() => {
 | 
						|
		let newBlockMap = new Map();
 | 
						|
		blocks.forEach((block) => {
 | 
						|
			newBlockMap.set(block.domain, block);
 | 
						|
		});
 | 
						|
		setBlockMap(newBlockMap);
 | 
						|
	}, [blocks]);
 | 
						|
 | 
						|
	const fileRef = React.useRef();
 | 
						|
 | 
						|
	function error(e) {
 | 
						|
		setOutput(<div className="error accent">{e}</div>);
 | 
						|
		throw e;
 | 
						|
	}
 | 
						|
 | 
						|
	function fileUpload() {
 | 
						|
		let reader = new FileReader();
 | 
						|
		reader.addEventListener("load", (e) => {
 | 
						|
			try {
 | 
						|
				// TODO: use validatem?
 | 
						|
				let json = JSON.parse(e.target.result);
 | 
						|
				json.forEach((block) => {
 | 
						|
					console.log("block:", block);
 | 
						|
				});
 | 
						|
			} catch(e) {
 | 
						|
				error(e.message);
 | 
						|
			}
 | 
						|
		});
 | 
						|
		reader.readAsText(fileRef.current.files[0]);
 | 
						|
	}
 | 
						|
 | 
						|
	React.useEffect(() => {
 | 
						|
		if (fileRef && fileRef.current) {
 | 
						|
			fileRef.current.addEventListener("change", fileUpload);
 | 
						|
		}
 | 
						|
		return function cleanup() {
 | 
						|
			fileRef.current.removeEventListener("change", fileUpload);
 | 
						|
		};
 | 
						|
	});
 | 
						|
 | 
						|
	function textImport() {
 | 
						|
		Promise.try(() => {
 | 
						|
			if (bulk[0] == "[") {
 | 
						|
				// assume it's json
 | 
						|
				return JSON.parse(bulk);
 | 
						|
			} else {
 | 
						|
				return bulk.split("\n").map((val) => {
 | 
						|
					return {
 | 
						|
						domain: val.trim()
 | 
						|
					};
 | 
						|
				});
 | 
						|
			}
 | 
						|
		}).then((domains) => {
 | 
						|
			console.log(domains);
 | 
						|
			let before = domains.length;
 | 
						|
			setOutput(`Importing ${before} domain(s)`);
 | 
						|
			domains = domains.filter(({domain}) => {
 | 
						|
				return (domain != "" && !blockMap.has(domain));
 | 
						|
			});
 | 
						|
			setOutput(<span>{output}<br/>{`Deduplicated ${before - domains.length}/${before} with existing blocks, adding ${domains.length} block(s)`}</span>);
 | 
						|
			if (domains.length > 0) {
 | 
						|
				let data = new FormData();
 | 
						|
				data.append("domains", new Blob([JSON.stringify(domains)], {type: "application/json"}), "import.json");
 | 
						|
				return oauth.apiRequest("/api/v1/admin/domain_blocks?import=true", "POST", data, "form");
 | 
						|
			}
 | 
						|
		}).then((json) => {
 | 
						|
			console.log("bulk import result:", json);
 | 
						|
			setBlocks(sortBlocks(deduplicateBlocks([...json, ...blocks])));
 | 
						|
		}).catch((e) => {
 | 
						|
			error(e.message);
 | 
						|
		});
 | 
						|
	}
 | 
						|
 | 
						|
	function textExport() {
 | 
						|
		setBulk(blocks.reduce((str, val) => {
 | 
						|
			if (typeof str == "object") {
 | 
						|
				return str.domain;
 | 
						|
			} else {
 | 
						|
				return str + "\n" + val.domain;
 | 
						|
			}
 | 
						|
		}));
 | 
						|
	}
 | 
						|
 | 
						|
	function jsonExport() {
 | 
						|
		Promise.try(() => {
 | 
						|
			return oauth.apiRequest("/api/v1/admin/domain_blocks?export=true", "GET");
 | 
						|
		}).then((json) => {
 | 
						|
			fileDownload(JSON.stringify(json), "block-export.json");
 | 
						|
		}).catch((e) => {
 | 
						|
			error(e);
 | 
						|
		});
 | 
						|
	}
 | 
						|
 | 
						|
	function textAreaUpdate(e) {
 | 
						|
		setBulk(e.target.value);
 | 
						|
	}
 | 
						|
 | 
						|
	return (
 | 
						|
		<React.Fragment>
 | 
						|
			<h3>Bulk import/export</h3>
 | 
						|
			<label htmlFor="bulk">Domains, one per line:</label>
 | 
						|
			<textarea value={bulk} rows={20} onChange={textAreaUpdate}></textarea>
 | 
						|
			<div className="controls">
 | 
						|
				<button onClick={textImport}>Import All From Field</button>
 | 
						|
				<button onClick={textExport}>Export To Field</button>
 | 
						|
				<label className="button" htmlFor="upload">Upload .json</label>
 | 
						|
				<button onClick={jsonExport}>Download .json</button>
 | 
						|
			</div>
 | 
						|
			{output}
 | 
						|
			<input type="file" id="upload" className="hidden" ref={fileRef}></input>
 | 
						|
		</React.Fragment>
 | 
						|
	);
 | 
						|
}
 | 
						|
 | 
						|
function AddBlock({oauth, blocks, setBlocks}) {
 | 
						|
	const [domain, setDomain] = React.useState("");
 | 
						|
	const [type, setType] = React.useState("suspend");
 | 
						|
	const [obfuscated, setObfuscated] = React.useState(false);
 | 
						|
	const [privateDescription, setPrivateDescription] = React.useState("");
 | 
						|
	const [publicDescription, setPublicDescription] = React.useState("");
 | 
						|
 | 
						|
	function addBlock() {
 | 
						|
		console.log(`${type}ing`, domain);
 | 
						|
		Promise.try(() => {
 | 
						|
			return oauth.apiRequest("/api/v1/admin/domain_blocks", "POST", {
 | 
						|
				domain: domain,
 | 
						|
				obfuscate: obfuscated,
 | 
						|
				private_comment: privateDescription,
 | 
						|
				public_comment: publicDescription
 | 
						|
			}, "json");
 | 
						|
		}).then((json) => {
 | 
						|
			setDomain("");
 | 
						|
			setPrivateDescription("");
 | 
						|
			setPublicDescription("");
 | 
						|
			setBlocks([json, ...blocks]);
 | 
						|
		});
 | 
						|
	}
 | 
						|
 | 
						|
	function onDomainChange(e) {
 | 
						|
		setDomain(e.target.value);
 | 
						|
	}
 | 
						|
 | 
						|
	function onTypeChange(e) {
 | 
						|
		setType(e.target.value);
 | 
						|
	}
 | 
						|
 | 
						|
	function onKeyDown(e) {
 | 
						|
		if (e.key == "Enter") {
 | 
						|
			addBlock();
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return (
 | 
						|
		<React.Fragment>
 | 
						|
			<h3>Add Block:</h3>
 | 
						|
			<div className="addblock">
 | 
						|
				<input id="domain" placeholder="instance" onChange={onDomainChange} value={domain} onKeyDown={onKeyDown} />
 | 
						|
				<select value={type} onChange={onTypeChange}>
 | 
						|
					<option id="suspend">Suspend</option>
 | 
						|
					<option id="silence">Silence</option>
 | 
						|
				</select>
 | 
						|
				<button onClick={addBlock}>Add</button>
 | 
						|
				<div>
 | 
						|
					<label htmlFor="private">Private description:</label><br/>
 | 
						|
					<textarea id="private" value={privateDescription} onChange={(e) => setPrivateDescription(e.target.value)}></textarea>
 | 
						|
				</div>
 | 
						|
				<div>
 | 
						|
					<label htmlFor="public">Public description:</label><br/>
 | 
						|
					<textarea id="public" value={publicDescription} onChange={(e) => setPublicDescription(e.target.value)}></textarea>
 | 
						|
				</div>
 | 
						|
				<div className="single">
 | 
						|
					<label htmlFor="obfuscate">Obfuscate:</label>
 | 
						|
					<input id="obfuscate" type="checkbox" value={obfuscated} onChange={(e) => setObfuscated(e.target.checked)}/>
 | 
						|
				</div>
 | 
						|
			</div>
 | 
						|
		</React.Fragment>
 | 
						|
	);
 | 
						|
}
 | 
						|
 | 
						|
// function Blocklist() {
 | 
						|
// 	return (
 | 
						|
// 		<section className="blocklists">
 | 
						|
// 			<h1>Blocklists</h1>
 | 
						|
// 		</section>
 | 
						|
// 	);
 | 
						|
// }
 |