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 2cf4d6e677f45805df7cfbb7606622a3a703aced
parent 4801d81951128756c6690ed97363bd8b0c6fbb4b
Author: MTRNord <mtrnord1@gmail.com>
Date:   Thu,  3 Mar 2022 16:50:31 +0100

Prepare ability to add bot to new room and allow smarter filtering of the dropwdown using a custom state event.

Diffstat:
MREADME.md | 30++++++++++++++++++++++++++++--
Msrc/components/App.tsx | 26++++++++++++++++++++++++--
Asrc/components/NewRoom.tsx | 29+++++++++++++++++++++++++++++
Msrc/components/forms/BanForm.tsx | 15++++++++++++++-
Asrc/components/forms/NewRoomForm.tsx | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/index.tsx | 8++++++--
Msrc/windowExt.ts | 14++++++++++++++
7 files changed, 202 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md @@ -28,4 +28,30 @@ Something like this (room_id and https are required) ## Hosted instance A hosted widget is at https://moderation_widget.nordgedanken.dev/ you can add it via -`/addwidget https://moderation_widget.nordgedanken.dev?room_id=$matrix_room_id` -\ No newline at end of file +`/addwidget https://moderation_widget.nordgedanken.dev?room_id=$matrix_room_id` + +## Adcanced + +### Use a state event to allow showing relevant Lists only in the dropdown + +TODO: + +1. Allow the bot to send the state event. For this you need to make the bot PL 1 +and allow sending `dev.nordgedanken.mjolnir_banlists` for PL1 users. +2. Send the following state event as the bot: + +You can use `/devtools` in element-web to send a custom state event. + +Event Type: `dev.nordgedanken.mjolnir_banlists` +State Key: The user id of the bot +Content: + +``` +{ + "banlists": { + "#matrix-org-coc-bl:matrix.org": "COC" + } +} +``` + +Edit it to fit your banlists. +\ No newline at end of file diff --git a/src/components/App.tsx b/src/components/App.tsx @@ -1,8 +1,9 @@ import { PureComponent } from 'react'; import BanList from './Banlist'; import Home from './Home'; +import NewRoom from './NewRoom'; -type Pages = "banlist" | "home"; +type Pages = "banlist" | "home" | "new_room"; type Props = Record<string, unknown>; type State = { @@ -29,6 +30,26 @@ class App extends PureComponent<Props, State> { }); } + onNewRoomClick() { + this.setState({ + current_page: "new_room" + }); + } + + getPage(page: Pages) { + switch (page) { + case "home": { + return <Home />; + } + case "banlist": { + return <BanList />; + } + case "new_room": { + return <NewRoom />; + } + } + } + render() { const current_page = this.state.current_page; return ( @@ -37,13 +58,14 @@ class App extends PureComponent<Props, State> { <div className='flex lg:flex-1 items-center justify-between lg:flex-row flex-col'> <nav className='flex lg:flex-shrink-0 my-4'> <button className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all' onClick={this.onHomeClick.bind(this)}>Home</button> + {/*<button className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all' onClick={this.onNewRoomClick.bind(this)}>Add to new Room</button> */} <button className='px-4 h-auto min-w-[1.5rem] flex items-center whitespace-nowrap cursor-pointer text-gray-900 dark:text-gray-200 font-medium brightness-100 hover:brightness-75 duration-200 ease-in-out transition-all' onClick={this.onBanListClick.bind(this)}>BanList</button> </nav> </div> </header> <div className='lg:pt-20 pt-52 z-0 max-h-full'> { - current_page === "home" ? <Home /> : (current_page === "banlist" ? <BanList /> : <Home />) + this.getPage(current_page) } </div> </div> diff --git a/src/components/NewRoom.tsx b/src/components/NewRoom.tsx @@ -0,0 +1,28 @@ +import { PureComponent } from "react"; +import NewRoomForm from "./forms/NewRoomForm"; + +type Props = Record<string, unknown>; +type State = Record<string, unknown>; + +export default class NewRoom extends PureComponent<Props, State> { + render() { + return ( + <main className="m-8"> + <h1 className="text-3xl font-bold underline text-gray-900 dark:text-gray-200 mb-4">Add new Room</h1> + <div className='flex flex-row justify-center items-center flex-wrap'> + <div className='flex flex-row flex-wrap justify-start'> + <section className='mr-16 mb-16'> + <NewRoomForm /> + </section> + <section className='mr-16 mb-16'> + <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">What will this do?</h3> + <p className="text-base font-normal text-gray-900 dark:text-gray-200 mb-2"> + TODO explenation + </p> + </section> + </div> + </div> + </main> + ); + } +} +\ No newline at end of file diff --git a/src/components/forms/BanForm.tsx b/src/components/forms/BanForm.tsx @@ -2,6 +2,8 @@ import { PureComponent } from "react"; import { CanonicalAliasEvent, CANONICAL_ALIAS, + DEV_NORDGEDANKEN_MJOLNIR_BANLISTS, + MJjolnirBanlists, M_POLICY_RULE_SERVER, M_POLICY_RULE_SERVER_ALT, M_POLICY_RULE_SERVER_OLD, @@ -46,6 +48,7 @@ class BanForm extends PureComponent<Props, State> { 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 banlist_codes = await window.widget_api.readStateEvents(DEV_NORDGEDANKEN_MJOLNIR_BANLISTS, 250_000_000_000, undefined, [this.state.roomId ?? "*"]) as MJjolnirBanlists[]; 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 listrooms = [...new Set([...user_rules_all.map(element => element.room_id), ...server_rules_all.map(element => element.room_id)])]; @@ -54,8 +57,18 @@ class BanForm extends PureComponent<Props, State> { const aliases_shortcodes = new Map(); for (const event of aliases) { - if (event.content?.alias) { + let result_known = true; + if (banlist_codes.length > 0) { + if (banlist_codes.some(known_lists_event => known_lists_event.content?.banlists.keys.includes(event.room_id) || known_lists_event.content?.banlists.keys.includes(event.content?.alias ?? ""))) { + result_known = true; + } else { + result_known = false; + } + } + if (event.content?.alias && result_known) { aliases_shortcodes.set(event.content?.alias, shortcodes.find(shortcode_event => shortcode_event.room_id === event.room_id)?.content?.shortcode); + } else if (result_known) { + aliases_shortcodes.set(event.room_id, shortcodes.find(shortcode_event => shortcode_event.room_id === event.room_id)?.content?.shortcode); } } diff --git a/src/components/forms/NewRoomForm.tsx b/src/components/forms/NewRoomForm.tsx @@ -0,0 +1,86 @@ +import { PureComponent } from "react"; +import { DEV_NORDGEDANKEN_MJOLNIR_BANLISTS, MJjolnirBanlists, M_ROOM_MEMBER, M_ROOM_POWERLEVELS } from "../../windowExt"; +type Props = Record<string, unknown>; +type State = { + room: string; + roomId?: string; + user_id: string; +}; +class NewRoomForm 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 = { + room: "", + roomId: roomId, + user_id: "" + }; + } + + async componentDidMount() { + // TODO use to get the bot if available + const banlist_codes = await window.widget_api.readStateEvents(DEV_NORDGEDANKEN_MJOLNIR_BANLISTS, 250_000_000_000, undefined, [this.state.roomId ?? "*"]) as MJjolnirBanlists[]; + } + + 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(); + + if (this.state.user_id === "" || this.state.room === "") { + return; + } + if (this.state.room.startsWith("!")) { + // TODO patch power_levels + // TODO fix race due to perms + window.widget_api.requestCapabilityToSendState(M_ROOM_MEMBER); + window.widget_api.requestCapabilityToReceiveState(M_ROOM_POWERLEVELS); + window.widget_api.requestCapabilityToSendState(M_ROOM_POWERLEVELS); + await window.widget_api.updateRequestedCapabilities(); + await window.widget_api.sendStateEvent(M_ROOM_MEMBER, this.state.user_id, { + displayname: "Administrator", + is_direct: false, + membership: "invite" + }, this.state.room); + } + + await window.widget_api.sendRoomEvent("m.room.message", { + msgtype: "m.text", + body: `!mjolnir rooms add ${this.state.room}`, + }, this.state.roomId); + } + + render() { + const { room, user_id } = this.state; + return ( + <> + <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-200 mb-2">Protect a new Room</h3> + <form onSubmit={this.handleSubmit.bind(this)} className="grid grid-cols-1 gap-4"> + <label className="block w-96"> + <span className="text-gray-900 dark:text-gray-200 visually-hidden mb-2">Room (required)</span> + <div className="mt-1 w-full flex flex-row box-border items-center cursor-text duration-300 max-w-full"> + <input required placeholder="Room (required)" 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">Bot user_id (required as we can't know)</span> + <div className="mt-1 w-full flex flex-row box-border items-center cursor-text duration-300 max-w-full"> + <input required placeholder="Bot user_id (required as we can't know)" 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_id" value={user_id} onChange={this.handleInputChange.bind(this)} /> + </div> + </label> + <input className="bg-green-400 hover:bg-green-500 cursor-pointer h-10 rounded dark:text-gray-900 text-gray-200 w-96" type="submit" value="Protect" /> + </form> + </> + ); + } +} + +export default NewRoomForm; +\ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx @@ -6,12 +6,15 @@ import reportWebVitals from './reportWebVitals'; import { WidgetApi } from 'matrix-widget-api'; import { CANONICAL_ALIAS, + DEV_NORDGEDANKEN_MJOLNIR_BANLISTS, 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, + M_ROOM_MEMBER, + M_TEXT, ORG_MATRIX_MJOLNIR_SHORTCODE, } from './windowExt'; @@ -26,8 +29,9 @@ window.widget_api.requestCapabilityToReceiveState(M_POLICY_RULE_USER_ALT); window.widget_api.requestCapabilityToReceiveState(M_POLICY_RULE_SERVER_ALT); window.widget_api.requestCapabilityToReceiveState(CANONICAL_ALIAS); window.widget_api.requestCapabilityToReceiveState(ORG_MATRIX_MJOLNIR_SHORTCODE); -window.widget_api.requestCapabilityToSendMessage("m.text"); -window.widget_api.requestCapabilityToReceiveState("m.room.member"); +window.widget_api.requestCapabilityToSendMessage(M_TEXT); +window.widget_api.requestCapabilityToReceiveState(M_ROOM_MEMBER); +window.widget_api.requestCapabilityToReceiveState(DEV_NORDGEDANKEN_MJOLNIR_BANLISTS); // TODO maybe figure out the proper way later window.widget_api.requestCapabilityForRoomTimeline("*"); diff --git a/src/windowExt.ts b/src/windowExt.ts @@ -12,6 +12,10 @@ export const M_POLICY_RULE_USER_ALT = "m.room.rule.user"; export const M_POLICY_RULE_SERVER_ALT = "m.room.rule.server"; export const CANONICAL_ALIAS = "m.room.canonical_alias"; export const ORG_MATRIX_MJOLNIR_SHORTCODE = "org.matrix.mjolnir.shortcode"; +export const M_TEXT = "m.text"; +export const M_ROOM_MEMBER = "m.room.member"; +export const M_ROOM_POWERLEVELS = "m.room.power_levels"; +export const DEV_NORDGEDANKEN_MJOLNIR_BANLISTS = "dev.nordgedanken.mjolnir_banlists"; export type UserRuleEvent = { sender: string; @@ -68,4 +72,14 @@ export type ShortcodeEvent = { content?: { shortcode: string; }; +}; + +export type MJjolnirBanlists = { + sender: string; + state_key: string; + event_id: string; + room_id: string; + content?: { + banlists: Record<string, string>; + }; }; \ No newline at end of file