gubes mirror. how does this work
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add gay sex (for pride month)

leah 8a2318a6 8e8ae338

+187 -17
+10 -7
core/connection.ts
··· 11 11 import list_channels from "./list"; 12 12 import { ChatBuffer } from "./buffer"; 13 13 import FileHost from "./filehost"; 14 + import Registration from "./registration"; 14 15 15 16 export interface ConnectionConfig { 16 17 /** ··· 111 112 Banned, 112 113 } 113 114 114 - const ping_interval = 30000; 115 + const pingterval = 30000; 115 116 116 117 export abstract class Connection { 117 118 constructor(public config: ConnectionConfig, opt?: ConnectionParameters) { ··· 165 166 $error: Signal<[code: ConnectionErrorCode, reason?: IrcMessage] | null> = signal(null); 166 167 167 168 sasl?: Sasl; 169 + registration: Registration = new Registration(this); 168 170 169 171 abstract connect(): Promise<void>; 170 172 ··· 229 231 #ping_token = "tubes"; 230 232 231 233 /** 232 - * begin pinging the server every {@link ping_interval} milliseconds. 234 + * begin pinging the server every {@link pingterval} milliseconds. 233 235 * 234 - * this is only used if the server doesn't bother pinging the client itself. 236 + * this is only used if the server doesn't bother pinging the client itself. update: this is no longer true. 237 + * not pinging causes many interesting issues in safari and also chrome sometimes. i love browsers 235 238 */ 236 239 protected start_pinging() { 237 240 const token = crypto.randomUUID(); 238 241 this.#ping_token = token; 239 242 240 243 setTimeout(() => { 241 - if (this.server_does_the_pinging) { 242 - return; 243 - } 244 + // if (this.server_does_the_pinging) { 245 + // return; 246 + // } 244 247 245 248 if (this.$state.value != ConnectionState.Connected) { 246 249 return; ··· 255 258 256 259 this.send(`PING ${this.#ping_token}`); 257 260 this.start_pinging(); 258 - }, ping_interval); 261 + }, pingterval); 259 262 } 260 263 261 264 /**
+1
core/handler.ts
··· 232 232 233 233 case Numeric.RPL_LOGGEDIN: { 234 234 connection.account_name = message.params?.[2]; 235 + connection.sasl!.authed = true; 235 236 break; 236 237 } 237 238 }
+2
core/isupport.ts
··· 1 1 import { IrcMessage } from "./parser"; 2 2 3 + // thought of the day: maybe strongly typed objects are overrated 4 + 3 5 /** 4 6 * everything that can be included in isupport 5 7 * as per the spec.
+25
core/registration.ts
··· 1 + import { Connection } from "."; 2 + import { Matcher } from "./queue"; 3 + 4 + export default class Registration { 5 + constructor(public conn: Connection) { } 6 + 7 + async register(account_name: string, password: string, email?: string) { 8 + this.conn.send(`REGISTER ${account_name} ${email || "*"} ${password}`); 9 + 10 + const result = await this.conn.expect("registration confirmation", 11 + new Matcher("REGISTER"), 12 + new Matcher("FAIL", "REGISTER") 13 + ); 14 + 15 + if (result.params?.[0] == "VERIFICATION_REQUIRED") { 16 + return (code: string) => { 17 + this.conn.send(`VERIFY ${account_name} ${code}`); 18 + return this.conn.expect("registration confirmation pt.2", 19 + new Matcher("VERIFY", "SUCCESS"), 20 + new Matcher("FAIL", "VERIFY"), 21 + ); 22 + } 23 + } 24 + } 25 + }
+4
core/sasl.ts
··· 117 117 static build_scram_sha256_digest(username: string, ) { 118 118 119 119 } 120 + 121 + get probably_insecure() { 122 + return !this.get_mechs().includes("SCRAM-SHA-256") 123 + } 120 124 }
+1 -1
neo/src/bits/dialog.tsx
··· 90 90 const dialog_spring_out = { type: "spring", visualDuration: .2, bounce: 0.1 }; 91 91 92 92 const trans_in = (dialog: HTMLDialogElement) => animate([ 93 - [dialog.children[0], { opacity: [0, 1], scale: [0.25, 1] }], 93 + [dialog.children[0], { opacity: [0, 1], scale: [0.75, 1] }], 94 94 [".dialog-scrim", { opacity: [0, 1] }, { at: 0 }], 95 95 ], { defaultTransition: dialog_spring }); 96 96
+10 -2
neo/src/bits/sidebar/network-section.tsx
··· 23 23 import EtcIcon from "~icons/ph/dots-three-bold"; 24 24 import DebugIcon from "~icons/ph/hammer"; 25 25 import RegisterIcon from "~icons/ph/identification-badge"; 26 + import LogoutIcon from "~icons/ph/stairs"; 26 27 import InfoIcon from "~icons/ph/info"; 27 28 import DisconnectedIcon from "~icons/ph/moon-fill"; 28 29 import DMIcon from "~icons/ph/paper-plane-right"; ··· 109 110 </MenuItem>} 110 111 111 112 112 - {conn.supports.sasl() && <MenuItem 113 + {conn.supports.sasl() && !conn.sasl?.authed && <MenuItem 113 114 onClick={() => set_location(`${connection_base(conn)}/auth/login`)} 114 115 icon={LoginIcon} 115 116 > 116 117 Log In 117 118 </MenuItem>} 118 119 119 - {conn.supports.registration() && <MenuItem 120 + {conn.supports.registration() && !conn.sasl?.authed && <MenuItem 120 121 onClick={() => set_location(`${connection_base(conn)}/auth/register`)} 121 122 icon={RegisterIcon} 122 123 > ··· 136 137 > 137 138 Configure 138 139 </MenuItem> 140 + 141 + {conn.supports.sasl() && conn.sasl?.authed && <MenuItem destructive 142 + // onClick={() => set_location(`${connection_base(conn)}/auth/register`)} 143 + icon={LogoutIcon} 144 + > 145 + Log Out 146 + </MenuItem>} 139 147 140 148 <MenuItem destructive icon={ArchiveIcon}>Archive</MenuItem> 141 149 </ul>);
+10 -2
neo/src/css/form.css
··· 6 6 --text-margin: .5rem; 7 7 margin: .5rem 0; 8 8 9 - +label.form-field { 9 + + label.form-field { 10 10 margin-top: 1rem; 11 11 } 12 12 ··· 44 44 border-color: var(--colour-accent-300); 45 45 outline: 3px solid var(--colour-accent-200); 46 46 } 47 + 48 + &:disabled { 49 + background-color: var(--colour-grey-100); 50 + border-color: var(--colour-grey-300); 51 + color: var(--colour-grey-950); 52 + cursor: not-allowed; 53 + background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23000000' fill-opacity='0.05' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E"); 54 + } 47 55 } 48 56 49 57 &:has(> input[type=checkbox]) { ··· 60 68 grid-row: 1; 61 69 margin: 0; 62 70 } 63 - 71 + 64 72 > .flavour { 65 73 grid-column: 2; 66 74 grid-row: 2;
+124 -5
neo/src/pages/auth.tsx
··· 1 1 import { useSignal } from "@preact/signals"; 2 2 import { PrimaryButton } from "@src/bits/buttons"; 3 + import { create_dialog, DialogInnards } from "@src/bits/dialog"; 3 4 import FancyDetails from "@src/bits/form/details"; 4 5 import FormField from "@src/bits/form/form-field"; 5 6 import NetworkHeader from "@src/bits/networkheader"; 6 7 import Spinner from "@src/bits/spinner"; 7 8 import conns, { connection_base } from "@src/chat/conns"; 8 - import { css } from "goober"; 9 + import { css, styled } from "goober"; 9 10 import { useRef } from "preact/hooks"; 10 11 import { Connection } from "tubes_core"; 11 12 import { IrcMessage } from "tubes_core/parser"; 12 13 import { useLocation } from "wouter-preact"; 13 14 import ErrorIcon from "~icons/ph/diamond-fill"; 15 + import WarningIcon from "~icons/ph/triangle-fill"; 14 16 15 17 export function LoginPage({ conn }: { conn: Connection }) { 16 18 const loading = useSignal(false); ··· 102 104 `}> 103 105 <p class="body-small"> 104 106 Log into your account on this network to access beautiful network features, 105 - reserved only for those who register. 107 + reserved only for those with the courage to register. 106 108 </p> 107 - <FormField label="Account Name"> 109 + <FormField label="Account Name" flavour_text={"Watch out! This is sometimes different from your nickname."}> 108 110 <input type="text" name="username" required min="1" /> 109 111 </FormField> 110 112 <FormField label="Password"> ··· 120 122 </FormField> 121 123 <p className="body-small">Looking for CertFP? We don't support it yet. Watch this space!</p> 122 124 </FancyDetails> 125 + {conn.sasl?.probably_insecure && <InfoBox> 126 + <h3 class="heading-2"><WarningIcon />ACHTUNG!</h3> 127 + <p class="body-small low-emphasis"> 128 + This network does not support secure password storage. Your password will be visible, unencrypted, naked to 129 + anyone with access to this computer. Please make extra sure not to reuse your bank password or what have you. 130 + </p> 131 + </InfoBox>} 123 132 {error.value && <EpicSaslFail conn={conn} error={error.value} reconnect={reconnect} />} 124 133 <PrimaryButton 125 134 style="width: max-content; justify-self: center;" ··· 130 139 </main> 131 140 } 132 141 142 + const InfoBox = styled("div")` 143 + background-color: var(--colour-yellow-100); 144 + padding: .5rem .75rem; 145 + margin: 0 -.75rem; 146 + border-radius: 6px; 147 + margin: auto; 148 + 149 + h3 { 150 + display: flex; 151 + gap: .25rem; 152 + align-items: center; 153 + color: var(--colour-yellow-900); 154 + 155 + svg { width: 12px; height: 12px; } 156 + } 157 + 158 + p { 159 + margin: 0; 160 + color: var(--colour-grey-800); 161 + } 162 + 163 + h4 { 164 + font-size: .85rem; 165 + font-weight: 400; 166 + font-variation-settings: 'GRAD' 150; 167 + margin-bottom: 0; 168 + } 169 + 170 + ul { 171 + margin-top: .5rem; 172 + 173 + button { 174 + all: unset; 175 + text-decoration: underline; 176 + font-variation-settings: 'GRAD' 150; 177 + cursor: pointer; 178 + color: var(--colour-red-900); 179 + } 180 + } 181 + `; 182 + 133 183 function EpicSaslFail({ error, conn, reconnect }: { error: unknown, conn: Connection, reconnect: () => void }) { 134 184 const text = error instanceof IrcMessage ? error.params?.at(-1) : error as string; 135 185 return <div role="alert" class={css` ··· 195 245 } 196 246 197 247 export function RegisterPage({ conn }: { conn: Connection }) { 248 + const tokens = conn.capabilities.get("draft/account-registration")?.split(","); 249 + const email_required = tokens?.includes("email-required"); 250 + const custom_account_name = tokens?.includes("custom-account-name"); 251 + 252 + const loading = useSignal(false); 253 + 254 + const verify_dialog = create_dialog(VerifyDialog, {}); 255 + const [, setLocation] = useLocation(); 256 + 198 257 return <main style="--width: 36rem;"> 199 258 <NetworkHeader conn={conn}> 200 259 <p>{conn.label}</p> ··· 210 269 211 270 width: 100%; 212 271 margin: auto; 213 - `}> 272 + `} 273 + onSubmit={async e => { 274 + e.preventDefault() 275 + loading.value = true; 276 + const data = 277 + Object.fromEntries(new FormData(e.currentTarget).entries()) as Record<string, string>; 278 + 279 + let { account, email, password } = data; 280 + console.log(account, email, password); 281 + 282 + if (!custom_account_name) { 283 + account = conn.nickname; 284 + } 285 + 286 + const verify = await conn.registration.register(account, password, email); 287 + 288 + if (!verify) { 289 + // todo: stuff 290 + console.log("no verify") 291 + conns.update_sasl(conn, account, { password }); 292 + setLocation(`${connection_base(conn)}/info`); 293 + 294 + return; 295 + } 296 + 297 + console.log(verify) 298 + }} 299 + > 214 300 <p class="body-small"> 215 301 Create an account on {conn.label} to access channels that require one, and reserve 216 - your nickname so people can't take it while you're disconnected. 302 + your nickname so people can't take it while you're disconnected. 217 303 </p> 304 + <FormField 305 + label="Account Name" 306 + flavour_text={!custom_account_name && <>{conn.label} says this has to be the same as your nickname, sorry.</>} 307 + > 308 + <input 309 + type="text" 310 + name="account" 311 + required 312 + min="1" 313 + placeholder={"e.g., the_grungler"} 314 + disabled={!custom_account_name} 315 + value={conn.nickname} 316 + /> 317 + </FormField> 318 + <FormField label="Password"> 319 + <input type="password" name="password" required min="1" placeholder={"e.g., ●●●●●●●●●●●●●●●"} /> 320 + </FormField> 321 + <FormField 322 + label="Email" 323 + flavour_text={!email_required && <>{conn.label} doesn't require you to provide this, but it might 324 + be helpful if you ever lose your password.</>} 325 + > 326 + <input type="email" name="email" required={email_required} min="1" placeholder={`e.g., ${conn.nickname}@gov.uk`} /> 327 + </FormField> 328 + <PrimaryButton 329 + style="width: max-content; justify-self: center;" 330 + type="submit" 331 + disabled={loading.value} 332 + >{loading.value ? <Spinner /> : "Register"}</PrimaryButton> 218 333 </form> 219 334 </main> 335 + } 336 + 337 + const VerifyDialog: DialogInnards = () => { 338 + 220 339 }