Chat: a GIF picker kept family-friendly, sharing a place, and Signal conversations in the Chat list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ddf9942c2
commit
584a95753d
16 files changed
+200
-13
No files matched your search
@@ -0,0 +1,48 @@
|
||||
// Family Chat extras: GIFs (kept family-friendly, fetched through the hub, kept in the chat) and sharing a place (a search, and coordinates that are checked).
|
||||
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-chatx-'));
|
||||
const GIF = Buffer.from('R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', 'base64'); let asked = [], tenorQ = [], giphyQ = [];
|
||||
const media = http.createServer((req, res) => { asked.push(req.url); if (req.url.startsWith('/big')) { res.writeHead(200, { 'content-type': 'image/gif' }); return res.end(Buffer.alloc(9 * 1024 * 1024)); } if (req.url.startsWith('/html')) { res.writeHead(200, { 'content-type': 'text/html' }); return res.end('<b>x</b>'); } res.writeHead(200, { 'content-type': 'image/gif' }); res.end(GIF); }).listen(0);
|
||||
const svc = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x'), M = 'http://127.0.0.1:' + media.address().port; res.setHeader('content-type', 'application/json');
|
||||
if (u.pathname.startsWith('/v2/')) { tenorQ.push(u.pathname + u.search); return res.end(JSON.stringify({ next: 'n2', results: [
|
||||
{ content_description: 'happy dance', tags: ['dance'], media_formats: { tinygif: { url: M + '/t1.gif', dims: [100, 80] }, gif: { url: M + '/g1.gif', dims: [300, 240] } } },
|
||||
{ content_description: 'sexy dance', tags: ['dance'], media_formats: { tinygif: { url: M + '/t2.gif', dims: [100, 80] }, gif: { url: M + '/g2.gif', dims: [300, 240] } } },
|
||||
{ content_description: 'party', tags: ['topless', 'fun'], media_formats: { tinygif: { url: M + '/t3.gif', dims: [100, 80] }, gif: { url: M + '/g3.gif', dims: [300, 240] } } },
|
||||
{ content_description: 'elsewhere', tags: [], media_formats: { tinygif: { url: 'https://evil.example/t4.gif', dims: [1, 1] }, gif: { url: 'https://evil.example/g4.gif', dims: [1, 1] } } }] })); }
|
||||
if (u.pathname.startsWith('/v1/gifs/')) { giphyQ.push(u.pathname + u.search); return res.end(JSON.stringify({ data: [{ title: 'cat', rating: 'g', images: { fixed_width_small: { url: M + '/s1.gif', width: '100', height: '80' }, fixed_width: { url: M + '/f1.gif' } } }, { title: 'cat 2', rating: 'pg-13', images: { fixed_width_small: { url: M + '/s2.gif', width: '100', height: '80' }, fixed_width: { url: M + '/f2.gif' } } }] })); }
|
||||
if (u.pathname === '/search') { return res.end(JSON.stringify([{ name: 'Central Park', display_name: 'Central Park, Manhattan, New York, United States', lat: '40.78', lon: '-73.96' }, { name: 'Bad', display_name: 'Bad', lat: 'x', lon: 'y' }])); }
|
||||
res.statusCode = 404; res.end('{}'); }).listen(0, async () => {
|
||||
process.env.GIF_MEDIA_HOSTS = '127.0.0.1:' + media.address().port; process.env.TENOR_URL = process.env.GIPHY_URL = process.env.NOMINATIM_URL = 'http://127.0.0.1:' + svc.address().port;
|
||||
const users = require('../users'), chat = require('../chat'), gifs = require('../gifs');
|
||||
const P = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['chat'], handle: 'Yg' }), c: users.create({ name: 'Cy', role: 'user', tabs: ['chat'], handle: 'Yw' }) };
|
||||
const hub = http.createServer((req, res) => users.runAs(P[req.headers['x-user'] || 'a'], () => chat.handle(req, res))).listen(0, async () => {
|
||||
const B = 'http://127.0.0.1:' + hub.address().port + '/api/chat', j = r => r.json(), call = (p, o = {}, who) => fetch(B + p, { ...o, headers: { ...(o.headers || {}), ...(who ? { 'x-user': who } : {}) } }), post = (p, b, who) => call(p, { method: 'POST', body: JSON.stringify(b) }, who);
|
||||
try {
|
||||
// the filter's words
|
||||
for (const bad of ['sexy dance', 'Boobs', 'naked man', 'p0rn'.replace('0', 'o'), 'bikini beach', 'F-U-C-K'.replace(/-/g, '')]) assert.equal(gifs.unsafe(bad), true, bad + ' is refused'); for (const ok of ['happy dance', 'class', 'assistant', 'Scunthorpe', 'cat', 'thanks', 'high five', 'book club', 'pass the butter']) assert.equal(gifs.unsafe(ok), false, ok + ' is fine');
|
||||
// not set up
|
||||
let r = await call('/gif/status'); assert.deepEqual(await j(r), { enabled: false, provider: '' }); assert.equal((await call('/gif?q=cat')).status, 503);
|
||||
// Tenor: always the strictest setting, and what comes back is filtered by words and by where it is hosted
|
||||
process.env.TENOR_API_KEY = 'k1'; assert.deepEqual(await j(await call('/gif/status')), { enabled: true, provider: 'tenor' });
|
||||
r = await j(await call('/gif?q=dance')); assert.match(tenorQ[0], /contentfilter=high/); assert.match(tenorQ[0], /q=dance/); assert.deepEqual(r.items.map(x => x.title), ['happy dance'], 'a title or a tag with a bad word, and a picture from another site, are dropped'); assert.equal(r.next, 'n2');
|
||||
assert.equal((await call('/gif?q=sexy')).status, 400, 'a bad search is refused before it is sent'); assert.equal(tenorQ.length, 1, 'and never reaches the service'); await call('/gif'); assert.match(tenorQ[1], /featured/, 'with no words: what is popular');
|
||||
// previews come through the hub (only ones it listed), never the phone's own request
|
||||
const prev = r.items[0].preview; assert.equal((await call('/gifimg?u=' + encodeURIComponent(prev))).status, 200); assert.equal((await call('/gifimg?u=' + encodeURIComponent('http://127.0.0.1:' + media.address().port + '/other.gif'))).status, 400, 'an address the hub did not list is refused'); assert.equal((await call('/gifimg?u=' + encodeURIComponent('https://evil.example/x.gif'))).status, 400);
|
||||
// sending: downloaded and kept in the chat like a photo
|
||||
const dm = (await j(await post('/new', { type: 'direct', with: P.b.id }))).conv; r = await post('/gif/send', { conv: dm.id, url: r.items[0].url, title: 'happy dance' }); assert.equal(r.status, 200); const m = (await j(r)).msg; assert.equal(m.att[0].type, 'image/gif'); assert.equal(m.att[0].name, 'happy dance.gif');
|
||||
const f = await call('/file/' + m.att[0].id, {}, 'b'); assert.equal(f.status, 200); assert.deepEqual(Buffer.from(await f.arrayBuffer()), GIF, 'Bob sees the very same picture, from the hub'); assert.equal((await post('/gif/send', { conv: dm.id, url: 'http://127.0.0.1:' + media.address().port + '/g1.gif' }, 'c')).status, 404, 'a stranger cannot send into the chat');
|
||||
assert.equal((await post('/gif/send', { conv: dm.id, url: 'http://127.0.0.1:' + media.address().port + '/never-listed.gif' })).status, 400, 'only listed GIFs can be sent');
|
||||
// size and type limits
|
||||
{ const M = 'http://127.0.0.1:' + media.address().port; gifs.remember([M + '/big.gif', M + '/html.gif']); assert.equal((await post('/gif/send', { conv: dm.id, url: M + '/big.gif' })).status, 413, 'a huge GIF is refused'); assert.equal((await post('/gif/send', { conv: dm.id, url: M + '/html.gif' })).status, 502, 'something that is not a picture is refused'); }
|
||||
// Giphy: G only, and nothing above it slips through
|
||||
delete process.env.TENOR_API_KEY; process.env.GIPHY_API_KEY = 'k2'; r = await j(await call('/gif?q=cat')); assert.match(giphyQ[0], /rating=g/); assert.match(giphyQ[0], /api_key=k2/); assert.deepEqual(r.items.map(x => x.title), ['cat'], 'a result rated above G is dropped even if the service sent it');
|
||||
// a place: search through the hub, and a share with checked coordinates
|
||||
assert.equal((await call('/places?q=ab')).status, 400); r = await j(await call('/places?q=central park')); assert.deepEqual(r.items, [{ title: 'Central Park', sub: 'Manhattan, New York, United States', lat: 40.78, lng: -73.96 }], 'bad rows are dropped');
|
||||
r = await post('/send', { conv: dm.id, share: { kind: 'place', title: 'Central Park', sub: 'New York', lat: 40.78, lng: -73.96, link: 'https://evil.example/' } }); assert.equal(r.status, 200); const sh = (await j(r)).msg.share; assert.equal(sh.link, 'https://www.openstreetmap.org/?mlat=40.78&mlon=-73.96#map=17/40.78/-73.96', 'the link is made by the hub, not taken from the sender'); assert.equal(sh.kind, 'place');
|
||||
for (const bad of [{ lat: 91, lng: 0 }, { lat: 0, lng: 181 }, { lat: 'x', lng: 1 }, { lat: '', lng: '' }, {}]) assert.equal((await post('/send', { conv: dm.id, share: { kind: 'place', title: 'x', ...bad } })).status, 400, JSON.stringify(bad) + ' is not a place');
|
||||
console.log('chat extras ok');
|
||||
} catch (e) { console.error(e); process.exitCode = 1; }
|
||||
hub.close(); svc.close(); media.close(); setTimeout(() => process.exit(process.exitCode || 0), 80);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ for (const f of listed) {
|
||||
}
|
||||
assert.deepEqual(failures, [], 'every script loads:\n ' + failures.join('\n '));
|
||||
// the pieces the app calls by name across files are there
|
||||
for (const name of ['route', 'viewHome', 'viewSettings', 'viewTasks', 'viewCalendar', 'viewBible', 'viewMail', 'viewRecipes', 'viewVideo', 'themeApply', 'autoSettings', 'bbToday', 'homeListening', 'saveToFiles', 'downloadText', 'viewPantry', 'pantryScan']) assert.equal(typeof vm.runInContext(`typeof ${name}`, ctx), 'string') && assert.equal(vm.runInContext(`typeof ${name}`, ctx), 'function', name + ' is defined');
|
||||
for (const name of ['route', 'viewHome', 'viewSettings', 'viewTasks', 'viewCalendar', 'viewBible', 'viewMail', 'viewRecipes', 'viewVideo', 'themeApply', 'autoSettings', 'bbToday', 'homeListening', 'saveToFiles', 'downloadText', 'viewPantry', 'pantryScan', 'chGifPicker', 'chPlacePicker', 'chSigRow']) assert.equal(typeof vm.runInContext(`typeof ${name}`, ctx), 'string') && assert.equal(vm.runInContext(`typeof ${name}`, ctx), 'function', name + ' is defined');
|
||||
// the watch-stats counting (a pure function)
|
||||
{ const now = Date.UTC(2026, 5, 15, 12), at = d => Math.floor((now - d * 864e5) / 1000), agg = vm.runInContext('vdAgg', ctx);
|
||||
const a = agg([{ lastViewedAt: at(1), duration: 6e6, Genre: [{ tag: 'Drama' }, { tag: 'Comedy' }] }, { lastViewedAt: at(3), duration: 3e6, Genre: [{ tag: 'Drama' }] }, { lastViewedAt: at(20), duration: 6e6 }, { lastViewedAt: at(200), duration: 6e6, Genre: [{ tag: 'Drama' }] }, { lastViewedAt: at(400), duration: 6e6 }, { duration: 6e6 }], now);
|
||||
|
||||
Reference in new issue
Block a user