matrix-moderation-widget

A widget for moderating with mjolnir. Highly WIP. Use at your own risk!
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-moderation-widget.git
Log | Files | Refs | README | LICENSE

commit 9ab751c0649f887a1c94bc4d3e4b2207a9a8e084
parent 409b2bf5ddf13248bb71556872c9fd15ad913df7
Author: MTRNord <mtrnord1@gmail.com>
Date:   Sun, 20 Feb 2022 22:01:50 +0100

Add redacting action

Diffstat:
AFeatureIdeas.md | 6++++++
Msrc/components/App.tsx | 4++--
Dsrc/components/Banlist.tsx | 157-------------------------------------------------------------------------------
Msrc/components/Home.tsx | 16++++++++++------
Asrc/components/forms/Banlist.tsx | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/forms/RedactForm.tsx | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 272 insertions(+), 165 deletions(-)

diff --git a/FeatureIdeas.md b/FeatureIdeas.md @@ -0,0 +1,5 @@ +# Ideas + +- Wordlist modifications +- Statistics +- Wordlist suggestions +\ No newline at end of file diff --git a/src/components/App.tsx b/src/components/App.tsx @@ -1,5 +1,5 @@ import { PureComponent } from 'react'; -import BanList from './Banlist'; +import BanList from './forms/Banlist'; import Home from './Home'; type Pages = "banlist" | "home"; @@ -41,7 +41,7 @@ class App extends PureComponent<Props, State> { </nav> </div> </header> - <div className='lg:pt-20 pt-52 z-0'> + <div className='lg:pt-20 pt-52 z-0 max-h-full'> { current_page === "home" ? <Home /> : (current_page === "banlist" ? <BanList /> : <Home />) } diff --git a/src/components/Banlist.tsx b/src/components/Banlist.tsx @@ -1,156 +0,0 @@ -import { PureComponent } from 'react'; -import { - MembershipEvent, - M_POLICY_RULE_SERVER, - M_POLICY_RULE_SERVER_ALT, - M_POLICY_RULE_SERVER_OLD, - M_POLICY_RULE_USER, - M_POLICY_RULE_USER_ALT, - M_POLICY_RULE_USER_OLD, - ServerRuleEvent, - UserRuleEvent -} from '../windowExt'; - -type Props = Record<string, unknown>; -type State = { - user_rules: UserRuleEvent[]; - server_rules: ServerRuleEvent[]; - roomId?: string; - filter?: string; - memberships: MembershipEvent[]; -}; - -class BanList extends PureComponent<Props, State> { - constructor(props: Props) { - super(props); - const urlParams = (new URL(window.location.href)).searchParams; - const roomId = urlParams.get("room_id") ?? undefined; - this.state = { - roomId: roomId, - user_rules: [], - server_rules: [], - memberships: [] - }; - } - - async componentDidMount() { - const user_rules = await window.widget_api.readStateEvents(M_POLICY_RULE_USER, 250_000_000_000, undefined, ["*"]) as UserRuleEvent[]; - const server_rules = await window.widget_api.readStateEvents(M_POLICY_RULE_SERVER, 250_000_000_000, undefined, ["*"]) as ServerRuleEvent[]; - const user_rules_old = await window.widget_api.readStateEvents(M_POLICY_RULE_USER_OLD, 250_000_000_000, undefined, ["*"]) as UserRuleEvent[]; - const server_rules_old = await window.widget_api.readStateEvents(M_POLICY_RULE_SERVER_OLD, 250_000_000_000, undefined, ["*"]) as ServerRuleEvent[]; - const user_rules_alt = await window.widget_api.readStateEvents(M_POLICY_RULE_USER_ALT, 250_000_000_000, undefined, ["*"]) as UserRuleEvent[]; - const server_rules_alt = await window.widget_api.readStateEvents(M_POLICY_RULE_SERVER_ALT, 250_000_000_000, undefined, ["*"]) as ServerRuleEvent[]; - const user_rules_all = [...user_rules, ...user_rules_old, ...user_rules_alt]; - const server_rules_all = [...server_rules, ...server_rules_old, ...server_rules_alt]; - const list_rooms = [...new Set([...user_rules_all.map(element => element.room_id), ...server_rules_all.map(element => element.room_id)])]; - const sender = new Set([...user_rules_all.map(element => element.sender), ...server_rules_all.map(element => element.sender)]); - const memberships = await window.widget_api.readStateEvents("m.room.member", 250_000_000_000, undefined, list_rooms) as MembershipEvent[]; - const memberships_needed = memberships.filter(member => sender.has(member.state_key)); - - this.setState({ - user_rules: user_rules_all, - server_rules: server_rules_all, - memberships: memberships_needed - }); - } - - onSearch(ev: { target: { value: string; }; }) { - this.setState({ - filter: ev.target.value - }); - } - - render() { - const { user_rules, server_rules, filter, memberships } = this.state; - const filtered_user_rules = filter ? user_rules.filter(element => element.content && Object.keys(element.content).length > 0 && element.content?.entity.includes(filter)) : user_rules; - const filtered_server_rules = filter ? server_rules.filter(element => element.content && Object.keys(element.content).length > 0 && element.content?.entity.includes(filter)) : server_rules; - return ( - <main className="m-8 flex items-center flex-col justify-center"> - <div className='flex flex-row justify-between content-between w-full'> - <h1 className="text-3xl font-bold underline text-gray-900 dark:text-gray-200 mb-4">Banlist data</h1> - <input onChange={this.onSearch.bind(this)} value={filter} placeholder="Search for user or server" className='py-1.5 px-2 rounded border-none placeholder:text-gray-900 text-gray-9000'></input> - </div> - <div className='flex flex-row justify-between content-between w-full'> - <section className='mr-2'> - <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">User Bans ({user_rules.length})</h3> - <table className='table-auto border-collapse border-2 border-slate-500 text-gray-900 dark:text-gray-200 text-base font-normal break-words'> - <thead> - <tr> - <th className='border-2 border-slate-600 p-1'>Entity</th> - <th className='border-2 border-slate-600 p-1'>Recommendation</th> - <th className='border-2 border-slate-600 p-1'>Reason</th> - <th className='border-2 border-slate-600 p-1'>Sender</th> - </tr> - </thead> - <tbody> - { - filtered_user_rules.length > 0 ? - filtered_user_rules.map(element => { - if (element.content && Object.keys(element.content).length > 0) { - const recommendation = element.content.recommendation === "m.ban" || element.content.recommendation === "org.matrix.mjolnir.ban" ? "Ban" : element.content.recommendation; - - // Entity is intentionally left as a mxid - const sender_dp = memberships.find(membership => membership.state_key === element.sender)?.content?.displayname ?? element.sender; - return ( - <tr key={`${element.event_id}`} className='border-2 border-slate-600'> - <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.entity}</td> - <td className='p-1 min-w-[1rem] max-w-[14rem]'>{recommendation}</td> - <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.reason}</td> - <td className='p-1 min-w-[12rem] max-w-[14rem]'>{sender_dp}</td> - </tr> - ); - } - }) : ( - <tr className='border-2 border-slate-600'> - <td className='p-1'>No user ban found</td> - </tr> - ) - } - </tbody> - </table> - </section> - <section> - <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">Server Bans ({server_rules.length})</h3> - <table className='table-auto border-collapse border-2 border-slate-500 text-gray-900 dark:text-gray-200 text-base font-normal break-words'> - <thead> - <tr> - <th className='border-2 border-slate-600 p-2'>Entity</th> - <th className='border-2 border-slate-600 p-2'>Recommendation</th> - <th className='border-2 border-slate-600 p-2'>Reason</th> - <th className='border-2 border-slate-600 p-1'>Sender</th> - </tr> - </thead> - <tbody> - { - filtered_server_rules.length > 0 ? - filtered_server_rules.map(element => { - if (element.content && Object.keys(element.content).length > 0) { - const recommendation = element.content.recommendation === "m.ban" || element.content.recommendation === "org.matrix.mjolnir.ban" ? "Ban" : element.content.recommendation; - - // Entity is intentionally left as a mxid - const sender_dp = memberships.find(membership => membership.state_key === element.sender)?.content?.displayname ?? element.sender; - return ( - <tr key={`${element.event_id}`} className='border-2 border-slate-600'> - <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.entity}</td> - <td className='p-1 min-w-[1rem] max-w-[14rem]'>{recommendation}</td> - <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.reason}</td> - <td className='p-1 min-w-[12rem] max-w-[14rem]'>{sender_dp}</td> - </tr> - ); - } - }) : ( - <tr className='border-2 border-slate-600'> - <td className='p-1'>No server ban found</td> - </tr> - ) - } - </tbody> - </table> - </section> - </div> - </main> - ); - } -} - -export default BanList; -\ No newline at end of file diff --git a/src/components/Home.tsx b/src/components/Home.tsx @@ -1,5 +1,6 @@ import { PureComponent } from 'react'; import BanForm from './BanForm'; +import RedactForm from './forms/RedactForm'; type Props = Record<string, unknown>; type State = Record<string, unknown>; @@ -10,12 +11,15 @@ class Home extends PureComponent<Props, State> { return ( <main className="m-8"> <h1 className="text-3xl font-bold underline text-gray-900 dark:text-gray-200 mb-4">Home</h1> - <div className='flex flex-row justify-between'> - <section> - <BanForm /> - </section> - <section> - </section> + <div className='flex flex-row justify-center items-center flex-wrap'> + <div className='flex flex-row flex-wrap'> + <section className='mr-16 mb-16'> + <BanForm /> + </section> + <section> + <RedactForm /> + </section> + </div> </div> </main> ); diff --git a/src/components/forms/Banlist.tsx b/src/components/forms/Banlist.tsx @@ -0,0 +1,156 @@ +import { PureComponent } from 'react'; +import { + MembershipEvent, + M_POLICY_RULE_SERVER, + M_POLICY_RULE_SERVER_ALT, + M_POLICY_RULE_SERVER_OLD, + M_POLICY_RULE_USER, + M_POLICY_RULE_USER_ALT, + M_POLICY_RULE_USER_OLD, + ServerRuleEvent, + UserRuleEvent +} from '../../windowExt'; + +type Props = Record<string, unknown>; +type State = { + user_rules: UserRuleEvent[]; + server_rules: ServerRuleEvent[]; + roomId?: string; + filter?: string; + memberships: MembershipEvent[]; +}; + +class BanList extends PureComponent<Props, State> { + constructor(props: Props) { + super(props); + const urlParams = (new URL(window.location.href)).searchParams; + const roomId = urlParams.get("room_id") ?? undefined; + this.state = { + roomId: roomId, + user_rules: [], + server_rules: [], + memberships: [] + }; + } + + async componentDidMount() { + const user_rules = await window.widget_api.readStateEvents(M_POLICY_RULE_USER, 250_000_000_000, undefined, ["*"]) as UserRuleEvent[]; + const server_rules = await window.widget_api.readStateEvents(M_POLICY_RULE_SERVER, 250_000_000_000, undefined, ["*"]) as ServerRuleEvent[]; + const user_rules_old = await window.widget_api.readStateEvents(M_POLICY_RULE_USER_OLD, 250_000_000_000, undefined, ["*"]) as UserRuleEvent[]; + const server_rules_old = await window.widget_api.readStateEvents(M_POLICY_RULE_SERVER_OLD, 250_000_000_000, undefined, ["*"]) as ServerRuleEvent[]; + const user_rules_alt = await window.widget_api.readStateEvents(M_POLICY_RULE_USER_ALT, 250_000_000_000, undefined, ["*"]) as UserRuleEvent[]; + const server_rules_alt = await window.widget_api.readStateEvents(M_POLICY_RULE_SERVER_ALT, 250_000_000_000, undefined, ["*"]) as ServerRuleEvent[]; + const user_rules_all = [...user_rules, ...user_rules_old, ...user_rules_alt]; + const server_rules_all = [...server_rules, ...server_rules_old, ...server_rules_alt]; + const list_rooms = [...new Set([...user_rules_all.map(element => element.room_id), ...server_rules_all.map(element => element.room_id)])]; + const sender = new Set([...user_rules_all.map(element => element.sender), ...server_rules_all.map(element => element.sender)]); + const memberships = await window.widget_api.readStateEvents("m.room.member", 250_000_000_000, undefined, list_rooms) as MembershipEvent[]; + const memberships_needed = memberships.filter(member => sender.has(member.state_key)); + + this.setState({ + user_rules: user_rules_all, + server_rules: server_rules_all, + memberships: memberships_needed + }); + } + + onSearch(ev: { target: { value: string; }; }) { + this.setState({ + filter: ev.target.value + }); + } + + render() { + const { user_rules, server_rules, filter, memberships } = this.state; + const filtered_user_rules = filter ? user_rules.filter(element => element.content && Object.keys(element.content).length > 0 && element.content?.entity.includes(filter)) : user_rules; + const filtered_server_rules = filter ? server_rules.filter(element => element.content && Object.keys(element.content).length > 0 && element.content?.entity.includes(filter)) : server_rules; + return ( + <main className="m-8 flex items-center flex-col justify-center"> + <div className='flex flex-row justify-between content-between w-full'> + <h1 className="text-3xl font-bold underline text-gray-900 dark:text-gray-200 mb-4">Banlist data</h1> + <input onChange={this.onSearch.bind(this)} value={filter} placeholder="Search for user or server" className='py-1.5 px-2 rounded border-none placeholder:text-gray-900 text-gray-9000'></input> + </div> + <div className='flex flex-row justify-between content-between w-full'> + <section className='mr-2'> + <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">User Bans ({user_rules.length})</h3> + <table className='table-auto border-collapse border-2 border-slate-500 text-gray-900 dark:text-gray-200 text-base font-normal break-words'> + <thead> + <tr> + <th className='border-2 border-slate-600 p-1'>Entity</th> + <th className='border-2 border-slate-600 p-1'>Recommendation</th> + <th className='border-2 border-slate-600 p-1'>Reason</th> + <th className='border-2 border-slate-600 p-1'>Sender</th> + </tr> + </thead> + <tbody> + { + filtered_user_rules.length > 0 ? + filtered_user_rules.map(element => { + if (element.content && Object.keys(element.content).length > 0) { + const recommendation = element.content.recommendation === "m.ban" || element.content.recommendation === "org.matrix.mjolnir.ban" ? "Ban" : element.content.recommendation; + + // Entity is intentionally left as a mxid + const sender_dp = memberships.find(membership => membership.state_key === element.sender)?.content?.displayname ?? element.sender; + return ( + <tr key={`${element.event_id}`} className='border-2 border-slate-600'> + <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.entity}</td> + <td className='p-1 min-w-[1rem] max-w-[14rem]'>{recommendation}</td> + <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.reason}</td> + <td className='p-1 min-w-[12rem] max-w-[14rem]'>{sender_dp}</td> + </tr> + ); + } + }) : ( + <tr className='border-2 border-slate-600'> + <td className='p-1'>No user ban found</td> + </tr> + ) + } + </tbody> + </table> + </section> + <section> + <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">Server Bans ({server_rules.length})</h3> + <table className='table-auto border-collapse border-2 border-slate-500 text-gray-900 dark:text-gray-200 text-base font-normal break-words'> + <thead> + <tr> + <th className='border-2 border-slate-600 p-2'>Entity</th> + <th className='border-2 border-slate-600 p-2'>Recommendation</th> + <th className='border-2 border-slate-600 p-2'>Reason</th> + <th className='border-2 border-slate-600 p-1'>Sender</th> + </tr> + </thead> + <tbody> + { + filtered_server_rules.length > 0 ? + filtered_server_rules.map(element => { + if (element.content && Object.keys(element.content).length > 0) { + const recommendation = element.content.recommendation === "m.ban" || element.content.recommendation === "org.matrix.mjolnir.ban" ? "Ban" : element.content.recommendation; + + // Entity is intentionally left as a mxid + const sender_dp = memberships.find(membership => membership.state_key === element.sender)?.content?.displayname ?? element.sender; + return ( + <tr key={`${element.event_id}`} className='border-2 border-slate-600'> + <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.entity}</td> + <td className='p-1 min-w-[1rem] max-w-[14rem]'>{recommendation}</td> + <td className='p-1 min-w-[8rem] max-w-[14rem]'>{element.content.reason}</td> + <td className='p-1 min-w-[12rem] max-w-[14rem]'>{sender_dp}</td> + </tr> + ); + } + }) : ( + <tr className='border-2 border-slate-600'> + <td className='p-1'>No server ban found</td> + </tr> + ) + } + </tbody> + </table> + </section> + </div> + </main> + ); + } +} + +export default BanList; +\ No newline at end of file diff --git a/src/components/forms/RedactForm.tsx b/src/components/forms/RedactForm.tsx @@ -0,0 +1,96 @@ +import { PureComponent } from "react"; +type Props = Record<string, unknown>; +type State = { + user: string; + event: string; + limit: string; + room: string; + roomId?: string; +}; +class RedactForm extends PureComponent<Props, State> { + constructor(props: Props | Readonly<Props>) { + super(props); + const urlParams = (new URL(window.location.href)).searchParams; + const roomId = urlParams.get("room_id") ?? undefined; + this.state = { + user: "", + event: "", + room: "", + limit: "", + roomId: roomId + }; + } + + handleInputChange(event: { target: { type: string, checked?: boolean, value: string | boolean, name: string; }; }) { + const target = event.target; + const value = target.type === 'checkbox' ? target.checked : target.value; + const name = target.name; + this.setState({ + [name]: value + } as unknown as State); + } + + async handleSubmit(ev: { preventDefault: () => void; }) { + ev.preventDefault(); + let command = ""; + if (this.state.event !== "" && this.state.user === "" && this.state.room === "" && this.state.limit === "") { + command = `!mjolnir redact ${this.state.event}`; + } else if (this.state.event === "" && this.state.user !== "" && this.state.room === "" && this.state.limit === "") { + command = `!mjolnir redact ${this.state.user}`; + } else if (this.state.event === "" && this.state.user !== "" && this.state.room !== "" && this.state.limit === "") { + command = `!mjolnir redact ${this.state.user} ${this.state.room}`; + } else if (this.state.event === "" && this.state.user !== "" && this.state.room !== "" && this.state.limit !== "") { + command = `!mjolnir redact ${this.state.user} ${this.state.room} ${this.state.limit}`; + } else if (this.state.event === "" && this.state.user !== "" && this.state.room === "" && this.state.limit !== "") { + command = `!mjolnir redact ${this.state.user} ${this.state.limit}`; + } else { + return; + } + await window.widget_api.sendRoomEvent("m.room.message", { + msgtype: "m.text", + body: command, + }, this.state.roomId); + } + + render() { + const { user, room, limit, event } = this.state; + return ( + <> + <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">Redact</h3> + <h4 className="text-gray-900 dark:text-gray-200 text-base font-thin mb-2 w-96 italic">(Note that this goes with the same combinations as the commands. Make sure empty fields are empty when submitting!)</h4> + <form onSubmit={this.handleSubmit.bind(this)} className="grid grid-cols-1 gap-4"> + <h4 className="text-gray-900 dark:text-gray-200 text-lg font-normal mb-1 w-96">Redacting a user</h4> + <label className="block w-96"> + <span className="text-gray-900 dark:text-gray-200 visually-hidden mb-2">User ID (required if Event Permalink empty)</span> + <div className="mt-1 w-full flex flex-row box-border items-center cursor-text duration-300 max-w-full"> + <input placeholder="User ID (required if Permalink empty)" className="rounded py-1.5 px-2 min-w-[1.25rem] flex-[1] border-none placeholder:text-gray-900 text-gray-900" type="text" name="user" value={user} onChange={this.handleInputChange.bind(this)} /> + </div> + </label> + <label className="block w-96"> + <span className="text-gray-900 dark:text-gray-200 visually-hidden mb-2">Room (optional)</span> + <div className="mt-1 w-full flex flex-row box-border items-center cursor-text duration-300 max-w-full"> + <input placeholder="Room (optional)" className="rounded py-1.5 px-2 min-w-[1.25rem] flex-[1] border-none placeholder:text-gray-900 text-gray-900" type="text" name="room" value={room} onChange={this.handleInputChange.bind(this)} /> + </div> + </label> + <label className="block w-96"> + <span className="text-gray-900 dark:text-gray-200 visually-hidden mb-2">Limit (optional)</span> + <div className="mt-1 w-full flex flex-row box-border items-center cursor-text duration-300 max-w-full"> + <input placeholder="Limit (optional)" className="rounded py-1.5 px-2 min-w-[1.25rem] flex-[1] border-none placeholder:text-gray-900 text-gray-900" type="text" name="limit" value={limit} onChange={this.handleInputChange.bind(this)} /> + </div> + </label> + <h4 className="text-gray-900 dark:text-gray-200 text-lg font-normal mb-1 w-96">Redacting an event</h4> + <label className="block w-96"> + <span className="text-gray-900 dark:text-gray-200 visually-hidden mb-2">Event Permalink (required if User ID empty)</span> + <div className="mt-1 w-full flex flex-row box-border items-center cursor-text duration-300 max-w-full"> + <input placeholder="Event Permalink (required if User ID empty)" className="rounded py-1.5 px-2 min-w-[1.25rem] flex-[1] border-none placeholder:text-gray-900 text-gray-900" type="text" name="event" value={event} onChange={this.handleInputChange.bind(this)} /> + </div> + </label> + + <input className="bg-blue-400 hover:bg-blue-500 cursor-pointer h-10 rounded dark:text-gray-900 text-gray-200 w-96" type="submit" value="Redact" /> + </form> + </> + ); + } +} + +export default RedactForm; +\ No newline at end of file