Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions SHA256SUMS
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ ecd8294e24a1cd55cc1aa0b3c5d6cde68c956524cfb1d7707766b4f6287b5e3e activity/index
2c22d3a5e0aff57971932e24cbdf4dc93c12f589b060203cc20417fd97a75142 activity/legal/legal.css
6c8b74d4d57bff38a393ab038418c8f410627dc66721bc416905289110f151ac activity/legal/privacy.html
297c926eb462285e52e196af2d23f2ac70cb280324bfd30aeff74836e02f159c activity/legal/terms.html
d2c03ccd185041e743b25a9c000bececdd2fc6464ed66b78a4d9e390070dbf3d activity/src/connection.ts
8e1c4a409e52f6677d2fb51c3afabae5de8de05be606b83a44271c6d31f35992 activity/src/main.ts
ac5a46a57d2c4ba90ed861c840b7240c9367359bbb21c8167890a7a61211f783 activity/src/connection.ts
df6b8511d942c38a498fc3a74b404d92556736be37e519021bb613871c1c2984 activity/src/main.ts
8010da4164c86394863cb645eaf4567f066822114c40edf1d2762c1a5da3806a activity/src/onboarding.ts
66988416b7ab216ad9d3947ed528bfe9f2e45f74943971d506a686ff0b044508 activity/style.css
3fb468ea85cd082f252d85b34bb3da8b62a10ae4aa984b48ef93f0ecacc7017f data/scams/README.md
Expand Down Expand Up @@ -440,7 +440,7 @@ f379be142cd234fb82c6b55ebb66e5ea94c184cb972cda576606d8dd872187ff starters/combi
d1611476abf0bc8ab0e1d4ccfea0abaa232183aa144390d56a8cf00f7468d984 starters/custom-template.typecheck.json
79ba12d8b758c4bcabc7c79b357cebc584442d3f2a368c2142576ef6a95975dd starters/image-scam-analysis.typecheck.json
20868d9d3d3760a4f19cc5aa97ef6496dd3e076507d9eb1b333a8f5ea9691d1e starters/reviewed-scam-references.typecheck.json
a791c7dc4bbecce78b91dc309287b2533156f11be43742fcbb89cf8ec285d542 tests/activity-connection.test.ts
28dda44df880208b1aaf2fe7454563d866fc83ac8096b0f5e88cd7c3ef2261c8 tests/activity-connection.test.ts
2257548bf1aa1de3113925dbdbd367db1211f5692ea6e0e69dae9830ef6ca611 tests/analytics.test.ts
977ac57a09795d86d5d5211ca91fa42db9e4f2b63442b0daadfb16025d42ef1c tests/compose.env
f9c22e2e2b1b12d1463e433966978b3cffb6f57e6903aae9d97d2d47a13eaed7 tests/compose.validation.yml
Expand Down
10 changes: 9 additions & 1 deletion activity/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@ export async function apiResponse<T = any>(response: Response): Promise<T> {
}
return data as T;
}
// Bot permissions requested at install: monitoring, warnings and the delete/timeout/kick/ban actions the bot checks for.
export const INSTALL_PERMISSIONS = '1374389611526';
export function installUrl(clientId: string, guildId?: string) {
const url = new URL('https://discord.com/oauth2/authorize');
url.search = new URLSearchParams({ client_id: clientId, scope: 'bot applications.commands', permissions: INSTALL_PERMISSIONS, integration_type: '0', ...(guildId ? { guild_id: guildId, disable_guild_select: 'true' } : {}) }).toString();
return url.href;
}
export class BotNotInServerError extends Error {}
export function connectionError(stage: string, error: unknown): Error {
if (error instanceof HttpError && error.status === 409 && error.message === 'bot_not_in_server')
return new Error('The TypeCheck bot is not in this server yet. A member with Manage Server needs to add it: open TypeCheck in a browser, click Add to your server, choose this server and authorize. Then click Connect in Discord.');
return new BotNotInServerError('The TypeCheck bot is not in this server yet. Connect in Discord only signs you in; it cannot add the bot. A member with Manage Server needs to click Add TypeCheck to this server, keep this server selected and authorize. Then click Connect in Discord.');
if (error instanceof HttpError && error.status === 403 && error.message === 'no_delegated_access')
return new Error('You need permission to use TypeCheck in this server. Ask a server owner or member with Manage Server to open TypeCheck → Permissions and grant access to your Discord role, then click Connect in Discord. Server owners and members with Administrator or Manage Server have access automatically.');
const detail = error instanceof HttpError ? ` (HTTP ${error.status}: ${error.message}${error.reference ? `; reference ${error.reference}` : ''})` :
Expand Down
13 changes: 9 additions & 4 deletions activity/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ async function packsPage(parent: HTMLElement) {
import { TEXT_STARTERS } from '../../shared/text-starters.js';
import { TEXT_LIMITS, parseText } from '../../shared/text.js';
import type { HistoricalScan } from '../../shared/historical.js';
import { activityAuth, apiResponse, HttpError, serializeConnection } from './connection.js';
import { activityAuth, apiResponse, BotNotInServerError, HttpError, installUrl, serializeConnection } from './connection.js';
import { timeoutPreset } from '../../shared/domain.js';
import { CONTENT_PRESETS, CONTENT_VERSION, parseContent } from '../../shared/content.js';
import { money, type AnalyticsReport, type Bucket, type Attempt, type Rate } from '../../shared/analytics.js';
Expand All @@ -144,6 +144,9 @@ interface SDK {
authenticate(v: {
access_token: string;
}): Promise<unknown>;
openExternalLink(v: {
url: string;
}): Promise<unknown>;
};
}
interface Metadata {
Expand Down Expand Up @@ -216,6 +219,10 @@ function message(value: string, error = false) { if (status) {
function fail(e: unknown) {
const text = e instanceof Error ? pretty(e.message) : 'Request failed';
message(text, true);
// An Activity can run in a server without the bot; offer Discord's install screen with this server preselected.
if (e instanceof BotNotInServerError && sdk && guild && status)
status.append(h('div', { class: 'actions' }, button('Add TypeCheck to this server', () => sdk!.commands.openExternalLink({ url: installUrl(connectionClientId, guild) }))),
h('small', {}, 'Requires the Manage Server permission in this server. Server owners and members with Administrator have it; otherwise ask one of them to add TypeCheck.'));
const open = Array.from(document.querySelectorAll<HTMLDialogElement>('dialog[open]')).at(-1);
if (open) {
open.querySelector('.modal-error')?.remove();
Expand Down Expand Up @@ -718,11 +725,9 @@ async function start(autoConnect = true) {
status.hidden = true;
const inDiscord = new URLSearchParams(location.search).has('frame_id');
const showInstall = !preview && !inDiscord;
const installUrl = new URL('https://discord.com/oauth2/authorize');
installUrl.search = new URLSearchParams({ client_id: config.clientId, scope: 'bot applications.commands', permissions: '1374389611526', integration_type: '0' }).toString();
app.append(h('main', { class: 'login' }, h('div', { class: 'login-brand' }, h('span', { class: 'brand-mark' }, 'J'), h('p', { class: 'eyebrow' }, 'JEV / TYPECHECK')), badge(preview ? 'OFFLINE PREVIEW' : 'EXPERIMENTAL MODERATION', 'amber'), h('h1', {}, 'Text + image moderation.\nUnder your control.'), h('p', { class: 'login-copy' }, 'Manage text policies, reference images, permissions, and credentials inside Discord. The backend bot handles continuous monitoring.'),
h('div', { class: 'actions login-actions' },
showInstall ? h('a', { class: 'button primary', href: installUrl.href, target: '_blank', rel: 'noopener noreferrer', 'aria-describedby': 'install-help' }, 'Add to your server') : null,
showInstall ? h('a', { class: 'button primary', href: installUrl(config.clientId), target: '_blank', rel: 'noopener noreferrer', 'aria-describedby': 'install-help' }, 'Add to your server') : null,
button(preview ? 'Open read-only preview' : 'Connect in Discord', () => connect(config.clientId), showInstall ? 'quiet' : 'primary')),
showInstall ? h('p', { class: 'install-help', id: 'install-help' }, 'Opens Discord in a new tab. You need Manage Server permission to install. Then open the TypeCheck Activity in your server; you can connect immediately using the same Manage Server permission to choose channels, add a Jev API key, and configure rules. No separate TypeCheck role is needed. Automatic moderation stays off until approved and enabled.') : null,
status, h('p', { class: 'muted' }, 'Launch from a server Activity. Server owners and members with Administrator or Manage Server have access automatically. Other members need a TypeCheck role grant.'),
Expand Down
15 changes: 13 additions & 2 deletions tests/activity-connection.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { activityAuth, apiResponse, connectionError, HttpError } from '../activity/src/connection.js';
import { activityAuth, apiResponse, BotNotInServerError, connectionError, HttpError, installUrl } from '../activity/src/connection.js';

test('missing TypeCheck role grant explains access requirements instead of retrying Discord authorization', () => {
const error = connectionError('Authorization code exchange', new HttpError('no_delegated_access', 403));
Expand Down Expand Up @@ -96,6 +96,17 @@ test('renewal permission or network failures do not start another OAuth flow', a
test('a server without the bot explains how to add it instead of reporting a server error', () => {
const error = connectionError('Authorization code exchange', new HttpError('bot_not_in_server', 409));
assert.match(error.message, /bot is not in this server yet/);
assert.match(error.message, /Add to your server/);
assert.ok(error instanceof BotNotInServerError);
assert.match(error.message, /only signs you in; it cannot add the bot/);
assert.match(error.message, /Add TypeCheck to this server/);
assert.doesNotMatch(error.message, /HTTP 409/);
});
test('the install link requests moderation permissions and can preselect the launching server', () => {
const general = new URL(installUrl('111'));
assert.equal(general.searchParams.get('permissions'), '1374389611526');
assert.equal(general.searchParams.get('scope'), 'bot applications.commands');
assert.equal(general.searchParams.get('guild_id'), null);
const server = new URL(installUrl('111', '222'));
assert.equal(server.searchParams.get('guild_id'), '222');
assert.equal(server.searchParams.get('disable_guild_select'), 'true');
});
Loading