toggle self-notification

secret dev setting

Changed files
+165 -46
atproto-notifications
src
components
setup
server
+72 -39
atproto-notifications/src/components/setup/Chrome.tsx
··· 1 import { Link } from 'react-router'; 2 import { Handle } from '../User'; 3 import './Chrome.css'; 4 5 export function Chrome({ user, onLogout, children }) { 6 - const content = children; 7 - const logout = () => null; 8 return ( 9 <> 10 - <header id="app-header"> 11 - <h1> 12 - <Link to="/" className="inherit-font"> 13 - spacedust notifications&nbsp;<span className="demo">demo!</span> 14 - </Link> 15 - </h1> 16 - {user && ( 17 - <div className="current-user"> 18 - <p> 19 - <span className="handle"> 20 - <Handle did={user.did} /> 21 - {user.role !== 'public' && ( 22 - <span className="chrome-role-tag"> 23 - {user.role === 'admin' ? ( 24 - <Link to="/admin" className="inherit-font">{user.role}</Link> 25 - ) : user.role === 'early' ? ( 26 - <Link to="/early" className="inherit-font">{user.role}</Link> 27 - ) : ( 28 - <>{user.role}</> 29 - )} 30 - </span> 31 - )} 32 - </span> 33 - <button className="subtle bad" onClick={onLogout}>&times;</button> 34 - </p> 35 - </div> 36 - )} 37 - </header> 38 39 <div id="app-content"> 40 - {content} 41 </div> 42 43 <div className="footer"> ··· 72 <p className="secret-dev"> 73 secret dev setting: 74 {' '} 75 - <label> 76 - <input 77 - type="checkbox" 78 - onChange={e => setDev(e.target.checked)} 79 - checked={true /*isDev(ufosHost)*/} 80 - /> 81 - localhost 82 - </label> 83 </p> 84 </div> 85 </>
··· 1 + import { useCallback, useEffect, useState } from 'react'; 2 import { Link } from 'react-router'; 3 import { Handle } from '../User'; 4 + import { GetJson, postJson } from '../Fetch'; 5 import './Chrome.css'; 6 7 + function Header({ user, onLogout }) { 8 + return ( 9 + <header id="app-header"> 10 + <h1> 11 + <Link to="/" className="inherit-font"> 12 + spacedust notifications&nbsp;<span className="demo">demo!</span> 13 + </Link> 14 + </h1> 15 + {user && ( 16 + <div className="current-user"> 17 + <p> 18 + <span className="handle"> 19 + <Handle did={user.did} /> 20 + {user.role !== 'public' && ( 21 + <span className="chrome-role-tag"> 22 + {user.role === 'admin' ? ( 23 + <Link to="/admin" className="inherit-font">{user.role}</Link> 24 + ) : user.role === 'early' ? ( 25 + <Link to="/early" className="inherit-font">{user.role}</Link> 26 + ) : ( 27 + <>{user.role}</> 28 + )} 29 + </span> 30 + )} 31 + </span> 32 + <button className="subtle bad" onClick={onLogout}>&times;</button> 33 + </p> 34 + </div> 35 + )} 36 + </header> 37 + ); 38 + } 39 + 40 export function Chrome({ user, onLogout, children }) { 41 + const [secretDevCounter, setSecretDevCounter] = useState(0); 42 + const [secretDevStatus, setSecretDevStatus] = useState(null); 43 + 44 + // ~~is this the best way~~ does it work? yeh 45 + const setSelfNotify = useCallback(async enabled => { 46 + setSecretDevStatus('pending'); 47 + const host = import.meta.env.VITE_NOTIFICATIONS_HOST; 48 + const url = new URL('/global-notify', host); 49 + try { 50 + await postJson(url, JSON.stringify({ notify_self: enabled }), true) 51 + setSecretDevStatus(null); 52 + } catch (err) { 53 + console.error('failed to set self-notify setting', err); 54 + setSecretDevStatus('failed'); 55 + } 56 + setSecretDevCounter(n => n + 1); 57 + }); 58 + 59 return ( 60 <> 61 + <Header user={user} onLogout={onLogout} /> 62 63 <div id="app-content"> 64 + {children} 65 </div> 66 67 <div className="footer"> ··· 96 <p className="secret-dev"> 97 secret dev setting: 98 {' '} 99 + <GetJson 100 + key={secretDevCounter} 101 + endpoint="/global-notify" 102 + credentials 103 + loading={() => <>&hellip;</>} 104 + ok={({ notify_self }) => ( 105 + <label> 106 + <input 107 + type="checkbox" 108 + onChange={e => setSelfNotify(e.target.checked)} 109 + checked={notify_self ^ (secretDevStatus === 'pending')} 110 + disabled={secretDevStatus === 'pending'} 111 + /> 112 + self-notify 113 + </label> 114 + )} 115 + /> 116 </p> 117 </div> 118 </>
+22 -1
server/api.js
··· 158 }; 159 160 const handleTopSecret = async (db, user, req, res) => { 161 - console.log('ts'); 162 // TODO: succeed early if they're already in? 163 const body = await getRequesBody(req); 164 const { secret_password } = JSON.parse(body); ··· 171 return forbidden(res); 172 } 173 }; 174 175 const handleListSecrets = async (db, res) => { 176 const secrets = db.getSecrets(); ··· 265 if (method === 'POST' && pathname === '/super-top-secret-access') { 266 if (!user) return unauthorized(res); 267 return handleTopSecret(db, user, req, res); 268 } 269 270 // non-public access required
··· 158 }; 159 160 const handleTopSecret = async (db, user, req, res) => { 161 // TODO: succeed early if they're already in? 162 const body = await getRequesBody(req); 163 const { secret_password } = JSON.parse(body); ··· 170 return forbidden(res); 171 } 172 }; 173 + 174 + const handleGetGlobalNotifySettings = async (db, user, res) => { 175 + const settings = db.getNotifyAccountGlobals(user.did); 176 + return ok(res, settings); 177 + }; 178 + 179 + const handleSetGlobalNotifySettings = async (db, user, req, res) => { 180 + const body = await getRequesBody(req); 181 + const { notify_enabled, notify_self } = JSON.parse(body); 182 + db.setNotifyAccountGlobals(user.did, { notify_enabled, notify_self }); 183 + return gotIt(res); 184 + }; 185 + 186 + /// admin stuff 187 188 const handleListSecrets = async (db, res) => { 189 const secrets = db.getSecrets(); ··· 278 if (method === 'POST' && pathname === '/super-top-secret-access') { 279 if (!user) return unauthorized(res); 280 return handleTopSecret(db, user, req, res); 281 + } 282 + if (method === 'GET' && pathname === '/global-notify') { 283 + if (!user) return unauthorized(res); 284 + return handleGetGlobalNotifySettings(db, user, res); 285 + } 286 + if (method === 'POST' && pathname === '/global-notify') { 287 + if (!user) return unauthorized(res); 288 + return handleSetGlobalNotifySettings(db, user, req, res); 289 } 290 291 // non-public access required
+31
server/db.js
··· 16 #stmt_delete_push_sub; 17 #stmt_get_push_info; 18 #stmt_set_role; 19 #stmt_admin_add_secret; 20 #stmt_admin_expire_secret; 21 #stmt_admin_get_secrets; ··· 111 where did = :did 112 and :secret_password in (select password 113 from top_secret_passwords)`); 114 115 this.#stmt_admin_add_secret = db.prepare( 116 `insert into top_secret_passwords (password) ··· 204 let res = this.#stmt_set_role.run(params); 205 return res.changes > 0; 206 } 207 208 addTopSecret(secretPassword) { 209 this.#stmt_admin_add_secret.run(secretPassword);
··· 16 #stmt_delete_push_sub; 17 #stmt_get_push_info; 18 #stmt_set_role; 19 + #stmt_get_notify_account_globals; 20 + #stmt_set_notify_account_globals; 21 + 22 #stmt_admin_add_secret; 23 #stmt_admin_expire_secret; 24 #stmt_admin_get_secrets; ··· 114 where did = :did 115 and :secret_password in (select password 116 from top_secret_passwords)`); 117 + 118 + this.#stmt_get_notify_account_globals = db.prepare( 119 + `select notify_enabled, 120 + notify_self 121 + from accounts 122 + where did = :did`); 123 + 124 + this.#stmt_set_notify_account_globals = db.prepare( 125 + `update accounts 126 + set notify_enabled = :notify_enabled, 127 + notify_self = :notify_self 128 + where did = :did`); 129 + 130 131 this.#stmt_admin_add_secret = db.prepare( 132 `insert into top_secret_passwords (password) ··· 220 let res = this.#stmt_set_role.run(params); 221 return res.changes > 0; 222 } 223 + 224 + getNotifyAccountGlobals(did) { 225 + return this.#stmt_get_notify_account_globals.get({ did }); 226 + } 227 + 228 + setNotifyAccountGlobals(did, globals) { 229 + this.#transactionally(() => { 230 + const update = this.getNotifyAccountGlobals(did); 231 + if (globals.notify_enabled !== undefined) update.notify_enabled = +globals.notify_enabled; 232 + if (globals.notify_self !== undefined) update.notify_self = +globals.notify_self; 233 + update.did = did; 234 + this.#stmt_set_notify_account_globals.run(update); 235 + }); 236 + } 237 + 238 239 addTopSecret(secretPassword) { 240 this.#stmt_admin_add_secret.run(secretPassword);
+38 -6
server/notifications.js
··· 84 } 85 }; 86 87 const handleDust = db => async event => { 88 console.log('got', event.data); 89 let data; ··· 100 } 101 const timestamp = +new Date(); 102 103 - let did; 104 - if (subject.startsWith('did:')) did = subject; 105 - else if (subject.startsWith('at://')) { 106 - const [id, ..._] = subject.slice('at://'.length).split('/'); 107 - if (id.startsWith('did:')) did = id; 108 - } 109 if (!did) { 110 console.warn(`ignoring link with non-DID subject: ${subject}`) 111 return; 112 } 113 114 const subs = db.getSubsByDid(did);
··· 84 } 85 }; 86 87 + const extractUriDid = at_uri => { 88 + if (!at_uri.startsWith('at://')) { 89 + console.warn(`ignoring non-at-uri: ${at_uri}`); 90 + return null; 91 + } 92 + const [id, ..._] = at_uri.slice('at://'.length).split('/'); 93 + if (!id) { 94 + console.warn(`ignoring at-uri with missing id segment: ${at_uri}`); 95 + return null; 96 + } 97 + if (id.startsWith('@')) { 98 + console.warn(`ignoring @handle at-uri: ${at_uri}`); 99 + return null; 100 + } 101 + if (!id.startsWith('did:')) { 102 + console.warn(`ignoring non-did at-uri: ${at_uri}`); 103 + return null; 104 + } 105 + return id; 106 + }; 107 + 108 const handleDust = db => async event => { 109 console.log('got', event.data); 110 let data; ··· 121 } 122 const timestamp = +new Date(); 123 124 + const did = subject.startsWith('did:') ? subject : extractUriDid(subject); 125 if (!did) { 126 console.warn(`ignoring link with non-DID subject: ${subject}`) 127 return; 128 + } 129 + 130 + // this works for now since only the account owner is assumed to be a notification target 131 + // but for "replies on post" etc that won't hold 132 + const { notify_enabled, notify_self } = db.getNotifyAccountGlobals(did); 133 + if (!notify_enabled) console.warn('would drop this since notifies are not enabled (ui todo)'); 134 + if (!notify_self) { 135 + const source_did = extractUriDid(source_record); 136 + if (!source_did) { 137 + console.warn(`ignoring link with non-DID source_record: ${source_record}`) 138 + return; 139 + } 140 + if (source_did === did) { 141 + console.warn(`ignoring self-notification`); 142 + return; 143 + } 144 } 145 146 const subs = db.getSubsByDid(did);
+2
server/schema.sql
··· 3 first_seen text not null default CURRENT_TIMESTAMP, 4 role text null, 5 secret_password text null, 6 7 check(did like 'did:%') 8 ) strict;
··· 3 first_seen text not null default CURRENT_TIMESTAMP, 4 role text null, 5 secret_password text null, 6 + notify_enabled integer not null default false, 7 + notify_self integer not null default false, 8 9 check(did like 'did:%') 10 ) strict;