Validate voice channel connect permissions (untested).

This commit is contained in:
ADAMJR 2022-12-17 03:39:12 +00:00
parent ea26910993
commit 4fae880e6d
33 changed files with 240 additions and 130 deletions

View File

@ -9,5 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Profanity filter: Text channel option to filter explicit words (disabled by default).
- Customizable themes: Create, share, and customize UI themes.
- Family Friendly Usernames: No profanity in usernames please.
- Send Files Permission: Control who can send files/images in a text channel.
- Invite Page: Join a server through `acrd.app/invites/<code>`.
- Advanced Invite Management: View who created roles, and how many uses they have had.
- Profanity Filter: Text channel option to filter explicit words (disabled by default).
- Customizable Themes: Create, share, and customize UI themes.

View File

@ -11,6 +11,12 @@ export default class Themes extends DBWrapper<string, ThemeDocument> {
throw new TypeError('Theme not found');
return theme;
}
public async getByCode(code: string | undefined) {
const theme = await Theme.findOne({ code });
if (!theme)
throw new TypeError('Theme not found');
return theme;
}
public async create(options: Partial<Entity.Theme>) {
this.parse(options.styles!);

View File

@ -62,8 +62,8 @@ router.delete('/:id', updateUser, validateUser, async (req, res) => {
res.status(201).json({ message: 'Deleted' });
});
router.get('/:id/unlock', updateUser, validateUser, async (req, res) => {
const theme = await deps.themes.get(req.params.id);
router.get('/:code/unlock', updateUser, validateUser, async (req, res) => {
const theme = await deps.themes.getByCode(req.params.code);
const user: SelfUserDocument = res.locals.user;
await deps.themes.unlock(theme.id, user);

View File

@ -12,9 +12,11 @@ export default class implements WSEvent<'CHANNEL_JOIN'> {
if (channel.type !== 'VOICE')
throw new TypeError('You cannot join a non-voice channel');
await deps.wsGuard.validateCanInChannel(client, channelId, 'CONNECT');
const userId = ws.sessions.get(client.id);
const user = await deps.users.getSelf(userId);
const movedChannel = user.voice.channelId !== channelId;
const movedChannel = (user.voice.channelId !== channelId);
if (user.voice.channelId && movedChannel)
await deps.channelLeave.invoke(ws, client);
@ -23,7 +25,6 @@ export default class implements WSEvent<'CHANNEL_JOIN'> {
if (doesExist)
throw new TypeError('User already connected to voice');
// TODO: perms - validate can join
deps.voiceService.add(channelId, { userId });
await Promise.all([

View File

@ -11,12 +11,12 @@ export default class implements WSEvent<'CHANNEL_LEAVE'> {
const userId = ws.sessions.get(client.id);
const user = await deps.users.getSelf(userId);
await this.updateVoiceState(user);
const oldChannel = await deps.channels.getSafely(user.voice.channelId);
const channelLeaveAction = (oldChannel)
? await this.handleExistingVC(oldChannel, userId, ws, client)
: undefined;
if (!oldChannel) return [];
await this.updateVoiceState(user);
var channelLeaveAction = await this.handleExistingVC(oldChannel, userId, ws, client);
return [channelLeaveAction, {
emit: 'VOICE_STATE_UPDATE' as const,
to: [client.id],
@ -34,9 +34,11 @@ export default class implements WSEvent<'CHANNEL_LEAVE'> {
// leave voice server
deps.voiceService.remove(oldChannel.id, userId);
await deps.channels.leaveVC(oldChannel, userId);
await client.leave(oldChannel.id);
await Promise.all([
client.leave(oldChannel.id),
deps.channels.leaveVC(oldChannel, userId),
]);
return {
emit: 'CHANNEL_UPDATE' as const,

View File

@ -18,6 +18,9 @@ export default class implements WSEvent<'MESSAGE_CREATE'> {
deps.users.getSelf(authorId),
]);
if (attachmentURLs && attachmentURLs.length > 0)
await deps.wsGuard.validateCanInChannel(client, channelId, 'SEND_FILES');
var message = await deps.messages.create(authorId, channelId, {
attachmentURLs,
content: this.filterContent(content, channel.filterProfanity),

View File

@ -1,7 +1,9 @@
import { faUpload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import usePerms from '../../../hooks/use-perms';
import { uploadFileAsMessage } from '../../../store/messages';
interface MessageBoxLeftSideProps {
@ -12,17 +14,21 @@ interface MessageBoxLeftSideProps {
const MessageBoxLeftSide: React.FunctionComponent<MessageBoxLeftSideProps> = (props) => {
const channel = useSelector((s: Store.AppState) => s.ui.activeChannel)!;
const dispatch = useDispatch();
const perms = usePerms();
const uploadInput = React.createRef<HTMLInputElement>();
const onChange: any = (e: Event) => {
const input = e.target as HTMLInputElement;
dispatch(uploadFileAsMessage(channel.id, { content: props.content }, input.files![0]));
dispatch(uploadFileAsMessage(channel.id, { content: props.content }, input.files![0]));
}
const canSendFiles = perms.canInChannel('SEND_FILES', channel.guildId, channel.id);
return (!props.editingMessageId) ? (
<div className="px-4">
<div className={classNames('px-4')}>
<div className="relative">
<input
disabled={!canSendFiles}
ref={uploadInput}
type="file"
name="file"
@ -30,9 +36,10 @@ const MessageBoxLeftSide: React.FunctionComponent<MessageBoxLeftSideProps> = (pr
onChange={onChange}
hidden />
<FontAwesomeIcon
color={canSendFiles ? '#ffffff' : 'var(--muted)'}
icon={faUpload}
onClick={() => uploadInput.current?.click()}
className="cursor-pointer z-1" />
className={classNames('cursor-pointer z-1')} />
</div>
</div>
) : null;

View File

@ -8,7 +8,7 @@ input:hover {
}
input:focus {
border: 1px solid var(--link);
border: 1px solid var(--primary);
}
input:disabled {

View File

@ -74,7 +74,7 @@ const ChannelSettingsPerms: React.FunctionComponent = () => {
overrideRoles.push(role);
return (
<div className="grid grid-cols-12 flex flex-col pt-14 px-10 pb-20 h-full mt-1">
<div className="grid grid-cols-12 flex-col pt-14 px-10 pb-20 h-full mt-1">
<div className="lg:col-span-3 col-span-12">
<nav className="pr-10">
{overrideRoles.map(r => (

View File

@ -20,11 +20,10 @@ const GuildSettingsInvites: React.FunctionComponent = () => {
{invites.map(i => (
<div className="w-full mb-3">
<strong><code>{i.id}</code></strong>
<span className="pl-5 secondary">
<span>Used <code>{i.uses}</code> times</span>
<span>Created by
<span className="ml-4 secondary">
<span className='ml-4'>Used <code>{i.uses}</code> times</span>
<span className='ml-4'>Created by
<Username
className='pt-2 scale-75'
size='sm'
user={guildUsers.find(gu => gu.id == i.inviterId)}
guild={guild} />

View File

@ -18,8 +18,8 @@ import Toggle from '../../inputs/toggle';
import SaveChanges from '../../utils/save-changes';
import TabLink from '../../utils/tab-link';
import RolePermissions from './role-permissions';
const GuildSettingsRoles: React.FunctionComponent = () => {
const GuildSettingsRoles: React.FunctionComponent = () => {
const dispatch = useDispatch();
const { handleSubmit, register, setValue, getValues } = useForm();
const { guildId }: any = useParams();
@ -62,20 +62,20 @@ const GuildSettingsRoles: React.FunctionComponent = () => {
<span>Separate role on member list</span>
<Toggle
id="hoisted"
className="float-right"
checked={hoisted}
{...register('hoisted')}
onChange={() => {
setHoisted(!hoisted);
setValue('hoisted', !hoisted);
}}
className="float-right" />
}} />
</span>
</div>
<RolePermissions
perms={perms}
setPerms={setPerms}
setRoleValue={setValue} />
setRoleValue={setValue} />
<NormalButton
onClick={() => dispatch(deleteRole(guildId, activeRole!.id))}
className="bg-danger float-right"
@ -87,12 +87,12 @@ const GuildSettingsRoles: React.FunctionComponent = () => {
const onSave = (e) => {
const onUpdate = (payload) => dispatch(updateRole(guildId, activeRole!.id, payload));
handleSubmit(onUpdate)(e);
};
};
const byPosition = (a, b) => (a.position > b.position) ? -1 : 1;
const selfIsHigher = (r) => permsService.memberIsHigher(guildId, [r.id]);
return (
<div className="grid grid-cols-12 flex flex-col pt-14 px-10 pb-20 h-full mt-1">
<div className="grid grid-cols-12 flex-col pt-14 px-10 pb-20 h-full mt-1">
<div className="lg:col-span-3 col-span-12">
<nav className="pr-10">
{roles.sort(byPosition).map(r => (
@ -126,9 +126,9 @@ const GuildSettingsRoles: React.FunctionComponent = () => {
setHoisted(activeRole!.hoisted);
}}
onSave={onSave}
obj={getValues()} />
obj={getValues()} />
</div>
);
}
export default GuildSettingsRoles;

View File

@ -20,12 +20,12 @@ const UserSettingsThemes: React.FunctionComponent = () => {
const [themeId, setTab] = useState(selfUser.activeThemeId);
const [addMode, enableAddMode] = useState(false);
const [focusedInputId, setFocusedInputId] = useState('');
const theme = getTheme(themeId, themes);
useEffect(() => {
if (!theme) setTab('default');
}, [theme, themeId]);
const refreshFocus = () => {
if (!focusedInputId) return;
@ -45,8 +45,9 @@ const UserSettingsThemes: React.FunctionComponent = () => {
}}
title={t.name}>
<SidebarIcon
childClasses={classNames('bg-bg-secondary', {
'border-2 border-primary h-[3.1rem]': t.id === themeId,
childClasses={classNames('border-2 h-[3.1rem] bg-bg-secondary', {
'border-primary': t.id === themeId,
'border-transparent': t.id !== themeId,
})}
imageURL={t.iconURL}
name={t.name}
@ -64,13 +65,13 @@ const UserSettingsThemes: React.FunctionComponent = () => {
const { register, setValue, handleSubmit } = useForm();
const theme = themes.find(t => t.id === themeId);
if (!theme) return null;
const onApply = () => dispatch(updateSelf({ activeThemeId: themeId }));
const onDelete = () => {
const confirmation = window.confirm('Are you sure you want to delete this theme?');
if (confirmation) dispatch(deleteTheme(theme.id));
};
const onSave = (e) => {
const onSave = (e) => {
const onUpdate = (payload) => dispatch(updateTheme(themeId, payload));
handleSubmit(onUpdate)(e);
};
@ -84,7 +85,7 @@ const UserSettingsThemes: React.FunctionComponent = () => {
<h1 className="text-3xl font-bold inline">Add Theme</h1>
<p className="secondary">Add an existing theme with a shareable code.</p>
</header>
<div className="mb-10">
<Input
className="float-left w-1/3 mr-3 disabled"
@ -115,23 +116,36 @@ const UserSettingsThemes: React.FunctionComponent = () => {
};
if (addMode) return <AddTheme />;
return (themeId) ? (
<div className="px-5 ml-4">
<header className="mb-5">
<h1 className="text-3xl font-bold inline">{theme.name}</h1>
</header>
{/* <FileInput
className="w-1/3"
name="icon"
label="Icon"
options={{ value: theme.iconURL }}
tooltip="An optional icon for your theme."
onChange={(e) => {
const file = e.currentTarget?.files?.[0];
if (!file) return;
dispatch(uploadFile(file, ({ url }) => {
dispatch(updateTheme(themeId, { iconURL: url }));
}));
}} /> */}
<form
onChange={() => dispatch(openSaveChanges(true))}
className="flex flex-col h-full mt-1 mb-5">
<header className="mb-5">
<h1 className="text-3xl font-bold inline">{theme.name}</h1>
</header>
<div className="flex">
<Input
className="w-1/3 mr-5"
label="Name"
name="name"
register={register}
setFocusedInputId={setFocusedInputId}
options={{ value: theme.name }} />
<Input
tooltip="The code that is used to share themes."
@ -140,30 +154,22 @@ const UserSettingsThemes: React.FunctionComponent = () => {
name="code"
register={register}
options={{ value: theme.code }}
setFocusedInputId={setFocusedInputId}
disabled />
<FileInput
className="w-1/3"
name="iconURL"
label="Icon"
options={{ value: theme.iconURL }}
tooltip="An optional icon for your theme."
setFocusedInputId={setFocusedInputId}
onChange={(e) => {
const file = e.currentTarget?.files?.[0];
if (!file) return;
dispatch(uploadFile(file, ({ url }) => {
dispatch(updateTheme(themeId, { iconURL: url }));
}));
}} />
</div>
<textarea
className="p-2 rounded bg-bg-secondary outline-none border-bg-tertiary hover:border w-1/2 mt-2"
defaultValue={theme.styles}
onFocus={(e) => setFocusedInputId?.(e.currentTarget.id)}
{...register('styles', { value: theme.styles })} />
<div className='mt-2'>
<label
htmlFor="styles"
className="uppercase text-xs font-semibold">Styles</label>
<textarea
id="styles"
rows={20}
className="p-2 rounded bg-bg-secondary outline-none w-full mt-2"
defaultValue={theme.styles}
onFocus={(e) => setFocusedInputId?.(e.currentTarget.id)}
{...register('styles', { value: theme.styles })} />
</div>
<SaveChanges
onOpen={refreshFocus}
@ -191,5 +197,5 @@ const UserSettingsThemes: React.FunctionComponent = () => {
</div>
);
}
export default UserSettingsThemes;

View File

@ -1,16 +1,16 @@
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import CircleButton from '../utils/buttons/circle-button';
const Navbar: React.FunctionComponent = () => {
const user = useSelector((s: Store.AppState) => s.auth.user);
return (
<nav className="flex items-center justify-between h-15 p-4 px-8">
<a className="logo">
<span className="font-bold text-white">accord</span>
<span className="text-gray-600">.</span>
<span className="muted font-light">app</span>
<span className="font-bold heading">acrd</span>
<span className="normal">.</span>
<span className="muted secondary">app</span>
</a>
<div>
<Link to={user ? '/channels/@me' : '/login'}>
@ -20,5 +20,5 @@ const Navbar: React.FunctionComponent = () => {
</nav>
);
}
export default Navbar;

View File

@ -7,7 +7,7 @@ import { getChannel, leaveVoiceChannel } from '../../../store/channels';
import { getGuild } from '../../../store/guilds';
import classNames from 'classnames';
import { useEffect } from 'react';
const SidebarFooter: React.FunctionComponent = () => {
const dispatch = useDispatch();
const user = useSelector((s: Store.AppState) => s.auth.user)!;
@ -19,7 +19,7 @@ const SidebarFooter: React.FunctionComponent = () => {
const ping = useSelector((s: Store.AppState) => s.meta.ping);
if (!channel || !guild) return null;
return (
<div id="sidebarFooter">
<div className="justify-between flex items-center p-3 pr-4">
@ -27,11 +27,11 @@ const SidebarFooter: React.FunctionComponent = () => {
<FontAwesomeIcon
icon={faSignal}
className={classNames({
'success': ping && ping < 100,
'secondary': ping && ping >= 100 && ping < 200,
'warning': ping && ping >= 200 && ping < 300,
'danger': ping && ping >= 300,
'muted': !ping,
// 'success': ping && ping < 100,
// 'secondary': ping && ping >= 100 && ping < 200,
// 'warning': ping && ping >= 200 && ping < 300,
// 'danger': ping && ping >= 300,
'success': true,
})} />
<strong className="success ml-2">Voice Connected</strong>
<div className="normal">{channel.name} / {guild.name}</div>
@ -45,7 +45,7 @@ const SidebarFooter: React.FunctionComponent = () => {
</div>
);
}
return (
<div className="bg-bg-secondary-alt">
<VoiceFooter />
@ -62,5 +62,5 @@ const SidebarFooter: React.FunctionComponent = () => {
</div>
);
}
export default SidebarFooter;

View File

@ -9,6 +9,7 @@ export interface SidebarIconProps {
to?: string;
childClasses?: string;
disableHoverEffect?: boolean;
styles?: any;
}
const SidebarIcon: React.FunctionComponent<SidebarIconProps> = (props) => {
@ -25,9 +26,9 @@ const SidebarIcon: React.FunctionComponent<SidebarIconProps> = (props) => {
const Icon = () => (imageURL)
? <Image
className="h-12 w-12"
src={imageURL}
alt={name} />
className="h-12 w-12"
src={imageURL}
alt={name} />
: <span className="select-none flex items-center justify-center h-12 w-12">{getAbbr(name)}</span>;
const isActive = to && location.pathname.startsWith(to);

View File

@ -10,7 +10,9 @@ import { fetchInvite, getInvite } from '../../store/invites';
import { joinGuild } from '../../store/members';
import { getTag, getUser } from '../../store/users';
import SidebarIcon from '../navigation/sidebar/sidebar-icon';
import Username from '../user/username';
import NormalButton from '../utils/buttons/normal-button';
import FullParticles from '../utils/full-particles';
import PageWrapper from './page-wrapper';
interface InvitePageProps { }
@ -48,17 +50,19 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
size="2x" />
);
if (!invite || !guild) return (
<Wrapper>
<NotFoundIcon />
<h1 className="text-xl font-bold warning">Invite not found...</h1>
<p className="lead">The invite either has expired, or never existed.</p>
</Wrapper>
);
if (!invite || !guild)
return (
<Wrapper>
<NotFoundIcon />
<h1 className="text-xl font-bold warning">Invite not found...</h1>
<p className="lead">The invite either has expired, or never existed.</p>
</Wrapper>
);
return (
<Wrapper>
<h1 className="text-3xl font-bold">You have been invited to {guild.name}!</h1>
<FullParticles />
<h1 className="text-3xl font-bold text-center">You have been invited to {guild.name}!</h1>
<div className="flex mt-5">
<SidebarIcon
name={guild.name}
@ -66,11 +70,13 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
childClasses="bg-bg-tertiary w-24 h-24 pt-6 text-xl"
disableHoverEffect />
<div className="flex justify-around items-center w-full mx-5">
<span>
<strong className="heading">Members</strong>: <code className="muted">{members.length}</code>
<span className='text-center'>
<div className="heading font-bold text-center">Members</div>
<code>{members.length}</code>
</span>
<span>
<strong className="heading">Owner</strong>: <code className="muted">{getTag(ownerUser)}</code>
<span className='text-center'>
<div className="heading font-bold">Owned By</div>
<Username user={ownerUser} />
</span>
</div>
</div>
@ -80,9 +86,9 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
dispatch(joinGuild(inviteId));
history.push(`/channels/${invite.guildId}`);
}}
className="bg-success light">Join :D</NormalButton>
className="bg-success dark">Join</NormalButton>
<Link to="/">
<NormalButton className="bg-danger light">Nope :(</NormalButton>
<NormalButton className="bg-danger light">Cancel</NormalButton>
</Link>
</div>
</Wrapper>

View File

@ -6,28 +6,24 @@ import fetchEntities from '../../store/actions/fetch-entities';
const LoadingPage: React.FunctionComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(ready());
dispatch(fetchEntities());
}, []);
const tips = [
'Did Discord steal our idea?!?!',
'ADAMJR, Stop refactoring code please. okthxbye.',
'Sample Text.',
'!(\'Hello World\')',
'May work on a Tesla',
'Please subscribe.',
'Hi YouTube!',
'accord.includes(\'VOICE_CHANNEL\') === true',
'Like Discord, but less cringe.',
'This message is officially dumb.',
'This message is funny.',
'Dear Bill Gates please buy my app okthxbye.',
'Is coding the same as programming? :thinking:',
'Is coding the same as programming? 🤔',
'TypeError: There may be bugs.',
'What is a Discord? :thinking:',
'What\'s your Skype? Wait, actually I don\'t want your IP. Cya.',
'Does anyone remember Skype?',
'Started in 2020.',
'Disclaimer: Not actually a Discord clone.',
];
@ -44,5 +40,5 @@ const LoadingPage: React.FunctionComponent = () => {
</PageWrapper>
);
}
export default LoadingPage;

View File

@ -19,10 +19,10 @@ const PrivateRoute: React.FunctionComponent<RouteProps> = (props) => {
const theme = themes.find(t => t.id === user.activeThemeId)
?? themes.find(t => t.id === 'default');
applyTheme(theme.styles);
return (
<Route {...props} />
);
}
export default PrivateRoute;

View File

@ -66,7 +66,7 @@ const FullParticles: React.FunctionComponent = () => {
},
"move": {
"enable": true,
"speed": 6,
"speed": 3,
"direction": "bottom",
"random": false,
"straight": false,

View File

@ -18,9 +18,10 @@ export class PermService {
'MANAGE_INVITES': 'Ability to delete invites.',
},
text: {
'SEND_MESSAGES': 'Ability to send messages in text channels.',
'MANAGE_MESSAGES': `Ability to manage message other member's messages.`,
'READ_MESSAGES': `Ability to read messages,`,
'SEND_MESSAGES': 'Ability to send messages in text channels.',
'SEND_FILES': `Ability to send files or images in messages.`,
},
};

View File

@ -27,7 +27,7 @@ const slice = createSlice({
creatorId: '177127942839676928',
iconURL: '/images/themes/discord.svg',
isFeatured: true,
name: 'Discord (experimental)',
name: 'Discord (built-in)',
styles: discordTheme,
}, {
id: 'winter',
@ -36,7 +36,7 @@ const slice = createSlice({
creatorId: '177127942839676928',
iconURL: '/images/themes/winter.svg',
isFeatured: true,
name: 'Winter (experimental)',
name: 'Winter (built-in)',
styles: winterTheme,
}] as Store.AppState['entities']['themes'],
reducers: {
@ -75,7 +75,7 @@ export const createTheme = (theme: Partial<Entity.Theme>, callback?: (theme: Ent
export const unlockTheme = (id: string, callback?: (theme: Entity.Theme) => any) => (dispatch) => {
dispatch(api.restCallBegan({
url: `/themes/unlock/${id}`,
url: `/themes/${id}/unlock`,
headers: getHeaders(),
callback,
}));

View File

@ -24,48 +24,74 @@ textarea {
:not(nav a)[href] {
color: var(--link);
}
nav a {
color: var(--channel);
}
nav a.active {
background-color: var(--bg-modifier-selected);
color: var(--heading);
}
textarea {
border: 1px solid var(--bg-secondary-alt);
caret-color: var(--heading);
}
textarea:hover {
border: 1px solid var(--bg-tertiary);
}
textarea:focus {
border: 1px solid var(--primary);
}
/* Util */
.light {
color: var(--light);
}
.dark {
color: var(--dark);
}
.font {
color: var(--font);
}
.success {
color: var(--success);
}
.normal {
color: var(--normal);
}
.muted {
color: var(--muted);
}
.danger {
color: var(--danger);
}
.warning {
color: var(--warning);
}
.heading {
color: var(--heading);
}
.primary {
color: var(--primary);
}
.secondary {
color: var(--secondary);
}
.tertiary {
color: var(--tertiary);
}
@ -73,36 +99,47 @@ nav a.active {
.bg-light {
background-color: var(--light);
}
.bg-dark {
background-color: var(--dark);
}
.bg-primary {
background-color: var(--primary);
}
.bg-secondary {
background-color: var(--secondary);
}
.bg-tertiary {
background-color: var(--tertiary);
}
.bg-warning {
background-color: var(--warning);
}
.bg-danger {
background-color: var(--danger);
}
.bg-success {
background-color: var(--success);
}
.bg-bg-primary {
background-color: var(--bg-primary);
}
.bg-bg-secondary {
background-color: var(--bg-secondary);
}
.bg-bg-secondary-alt {
background-color: var(--bg-secondary-alt);
}
.bg-bg-tertiary {
background-color: var(--bg-tertiary);
}
@ -112,12 +149,15 @@ nav a.active {
background-color: var(--bg-modifier-accent);
color: var(--heading);
}
.bg-bg-floating {
background-color: var(--bg-floating);
}
.shadow-elevation {
box-shadow: var(--elevation);
}
.font-primary {
font-family: var(--font-primary);
}
@ -126,36 +166,51 @@ nav a.active {
.border-light {
border-color: var(---light);
}
.border-dark {
border-color: var(--dark);
}
.border-muted {
border-color: var(--muted);
}
.border-primary {
border-color: var(--primary);
}
.border-secondary {
border-color: var(--secondary);
}
.border-tertiary {
border-color: var(--tertiary);
}
.border-danger {
border-color: var(--danger);
}
.border-success {
border-color: var(--success);
}
.border-bg-primary {
border-color: var(--bg-primary);
}
.border-bg-secondary {
border-color: var(--bg-secondary);
}
.border-bg-secondary-alt {
border-color: var(--bg-secondary-alt);
}
.border-bg-tertiary {
border-color: var(--bg-tertiary);
}
.border-transparent {
border-color: transparent !important;
}

View File

@ -10,6 +10,7 @@ export declare namespace PermissionTypes {
ADMINISTRATOR = 1
}
enum Text {
SEND_FILES = 16384,
READ_MESSAGES = 8192,
MANAGE_MESSAGES = 4096,
SEND_MESSAGES = 2048
@ -26,6 +27,7 @@ export declare namespace PermissionTypes {
MUTE_MEMBERS: Voice;
SPEAK: Voice;
CONNECT: Voice;
SEND_FILES: Text;
READ_MESSAGES: Text;
MANAGE_MESSAGES: Text;
SEND_MESSAGES: Text;

View File

@ -1,5 +1,4 @@
"use strict";
// REMINDER: 8 is admin in Discord, but 1 in Accord
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPermString = exports.PermissionTypes = void 0;
var PermissionTypes;
@ -22,7 +21,7 @@ var PermissionTypes;
let Text;
(function (Text) {
// ADD_REACTIONS = 2048 * 16,
// MENTION_EVERYONE = 2048 * 8,
Text[Text["SEND_FILES"] = 16384] = "SEND_FILES";
Text[Text["READ_MESSAGES"] = 8192] = "READ_MESSAGES";
Text[Text["MANAGE_MESSAGES"] = 4096] = "MANAGE_MESSAGES";
Text[Text["SEND_MESSAGES"] = 2048] = "SEND_MESSAGES";
@ -39,6 +38,7 @@ var PermissionTypes;
| PermissionTypes.General.CREATE_INVITE
| PermissionTypes.Text.SEND_MESSAGES
| PermissionTypes.Text.READ_MESSAGES
| PermissionTypes.Text.SEND_FILES
// | PermissionTypes.Text.ADD_REACTIONS
| PermissionTypes.Voice.CONNECT
| PermissionTypes.Voice.SPEAK;
@ -53,4 +53,4 @@ function getPermString(integer) {
: integer.toString();
}
exports.getPermString = getPermString;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVybWlzc2lvbnMudHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcGVybWlzc2lvbnMudHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLG9EQUFvRDs7O0FBRXBELElBQWlCLGVBQWUsQ0E0Qy9CO0FBNUNELFdBQWlCLGVBQWU7SUFDOUIsSUFBWSxPQWFYO0lBYkQsV0FBWSxPQUFPO1FBQ2pCLDBEQUFvQixDQUFBO1FBQ3BCLDJDQUEyQztRQUMzQywwQ0FBMEM7UUFDMUMsMkRBQW9CLENBQUE7UUFDcEIseURBQW1CLENBQUE7UUFDbkIsc0RBQWlCLENBQUE7UUFDakIscUNBQXFDO1FBQ3JDLDREQUFvQixDQUFBO1FBQ3BCLHFEQUFnQixDQUFBO1FBQ2hCLHFEQUFnQixDQUFBO1FBQ2hCLHVDQUF1QztRQUN2Qyx1REFBaUIsQ0FBQTtJQUNuQixDQUFDLEVBYlcsT0FBTyxHQUFQLHVCQUFPLEtBQVAsdUJBQU8sUUFhbEI7SUFDRCxJQUFZLElBTVg7SUFORCxXQUFZLElBQUk7UUFDZCw2QkFBNkI7UUFDN0IsK0JBQStCO1FBQy9CLG9EQUF3QixDQUFBO1FBQ3hCLHdEQUEwQixDQUFBO1FBQzFCLG9EQUFvQixDQUFBO0lBQ3RCLENBQUMsRUFOVyxJQUFJLEdBQUosb0JBQUksS0FBSixvQkFBSSxRQU1mO0lBQ0QsSUFBWSxLQUtYO0lBTEQsV0FBWSxLQUFLO1FBQ2Ysc0RBQXdCLENBQUE7UUFDeEIsc0RBQXdCLENBQUE7UUFDeEIsdUNBQWlCLENBQUE7UUFDakIsMkNBQWUsQ0FBQTtJQUNqQixDQUFDLEVBTFcsS0FBSyxHQUFMLHFCQUFLLEtBQUwscUJBQUssUUFLaEI7SUFDWSxtQkFBRyxpREFDWCxPQUFPLEdBQ1AsSUFBSSxHQUNKLEtBQUssQ0FDVCxDQUFBO0lBSVksa0NBQWtCLEdBQzdCLGVBQWUsQ0FBQyxPQUFPLENBQUMsYUFBYTtVQUNuQyxlQUFlLENBQUMsT0FBTyxDQUFDLGFBQWE7VUFDckMsZUFBZSxDQUFDLElBQUksQ0FBQyxhQUFhO1VBQ2xDLGVBQWUsQ0FBQyxJQUFJLENBQUMsYUFBYTtRQUNwQyx1Q0FBdUM7VUFDckMsZUFBZSxDQUFDLEtBQUssQ0FBQyxPQUFPO1VBQzdCLGVBQWUsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDO0FBQ2xDLENBQUMsRUE1Q2dCLGVBQWUsR0FBZix1QkFBZSxLQUFmLHVCQUFlLFFBNEMvQjtBQUVELFNBQWdCLGFBQWEsQ0FBQyxPQUF3Qjs7SUFDcEQsT0FBTyxDQUFDLE9BQU8sT0FBTyxLQUFLLFFBQVEsQ0FBQztRQUNsQyxDQUFDLENBQUMsTUFBQSxNQUFBLE1BQU07YUFDTCxPQUFPLENBQUMsZUFBZSxDQUFDLEdBQUcsQ0FBQzthQUM1QixNQUFNLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO2FBQ3hDLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEtBQUssT0FBTyxJQUFJLENBQUMsS0FBSyxPQUFPLENBQUMsMENBQUcsQ0FBQyxDQUFDLG1DQUFJLEVBQUU7UUFDOUQsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxRQUFRLEVBQUUsQ0FBQztBQUN6QixDQUFDO0FBUEQsc0NBT0MifQ==
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGVybWlzc2lvbnMudHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcGVybWlzc2lvbnMudHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsSUFBaUIsZUFBZSxDQTZDL0I7QUE3Q0QsV0FBaUIsZUFBZTtJQUM5QixJQUFZLE9BYVg7SUFiRCxXQUFZLE9BQU87UUFDakIsMERBQW9CLENBQUE7UUFDcEIsMkNBQTJDO1FBQzNDLDBDQUEwQztRQUMxQywyREFBb0IsQ0FBQTtRQUNwQix5REFBbUIsQ0FBQTtRQUNuQixzREFBaUIsQ0FBQTtRQUNqQixxQ0FBcUM7UUFDckMsNERBQW9CLENBQUE7UUFDcEIscURBQWdCLENBQUE7UUFDaEIscURBQWdCLENBQUE7UUFDaEIsdUNBQXVDO1FBQ3ZDLHVEQUFpQixDQUFBO0lBQ25CLENBQUMsRUFiVyxPQUFPLEdBQVAsdUJBQU8sS0FBUCx1QkFBTyxRQWFsQjtJQUNELElBQVksSUFNWDtJQU5ELFdBQVksSUFBSTtRQUNkLDZCQUE2QjtRQUM3QiwrQ0FBcUIsQ0FBQTtRQUNyQixvREFBd0IsQ0FBQTtRQUN4Qix3REFBMEIsQ0FBQTtRQUMxQixvREFBb0IsQ0FBQTtJQUN0QixDQUFDLEVBTlcsSUFBSSxHQUFKLG9CQUFJLEtBQUosb0JBQUksUUFNZjtJQUNELElBQVksS0FLWDtJQUxELFdBQVksS0FBSztRQUNmLHNEQUF3QixDQUFBO1FBQ3hCLHNEQUF3QixDQUFBO1FBQ3hCLHVDQUFpQixDQUFBO1FBQ2pCLDJDQUFlLENBQUE7SUFDakIsQ0FBQyxFQUxXLEtBQUssR0FBTCxxQkFBSyxLQUFMLHFCQUFLLFFBS2hCO0lBQ1ksbUJBQUcsaURBQ1gsT0FBTyxHQUNQLElBQUksR0FDSixLQUFLLENBQ1QsQ0FBQTtJQUlZLGtDQUFrQixHQUM3QixlQUFlLENBQUMsT0FBTyxDQUFDLGFBQWE7VUFDbkMsZUFBZSxDQUFDLE9BQU8sQ0FBQyxhQUFhO1VBQ3JDLGVBQWUsQ0FBQyxJQUFJLENBQUMsYUFBYTtVQUNsQyxlQUFlLENBQUMsSUFBSSxDQUFDLGFBQWE7VUFDbEMsZUFBZSxDQUFDLElBQUksQ0FBQyxVQUFVO1FBQ2pDLHVDQUF1QztVQUNyQyxlQUFlLENBQUMsS0FBSyxDQUFDLE9BQU87VUFDN0IsZUFBZSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7QUFDbEMsQ0FBQyxFQTdDZ0IsZUFBZSxHQUFmLHVCQUFlLEtBQWYsdUJBQWUsUUE2Qy9CO0FBRUQsU0FBZ0IsYUFBYSxDQUFDLE9BQXdCOztJQUNwRCxPQUFPLENBQUMsT0FBTyxPQUFPLEtBQUssUUFBUSxDQUFDO1FBQ2xDLENBQUMsQ0FBQyxNQUFBLE1BQUEsTUFBTTthQUNMLE9BQU8sQ0FBQyxlQUFlLENBQUMsR0FBRyxDQUFDO2FBQzVCLE1BQU0sQ0FBQyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7YUFDeEMsSUFBSSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsS0FBSyxPQUFPLElBQUksQ0FBQyxLQUFLLE9BQU8sQ0FBQywwQ0FBRyxDQUFDLENBQUMsbUNBQUksRUFBRTtRQUM5RCxDQUFDLENBQUMsT0FBTyxDQUFDLFFBQVEsRUFBRSxDQUFDO0FBQ3pCLENBQUM7QUFQRCxzQ0FPQyJ9

View File

@ -109,7 +109,7 @@ export namespace ChannelTypes {
}
export interface VoiceConnection {
userId: string;
blob?: Blob;
blob?: any;
}
}

View File

@ -1,5 +1,3 @@
// REMINDER: 8 is admin in Discord, but 1 in Accord
export namespace PermissionTypes {
export enum General {
VIEW_CHANNELS = 1024,
@ -16,8 +14,7 @@ export namespace PermissionTypes {
ADMINISTRATOR = 1,
}
export enum Text {
// ADD_REACTIONS = 2048 * 16,
// MENTION_EVERYONE = 2048 * 8,
SEND_FILES = 2048 * 8,
READ_MESSAGES = 2048 * 4,
MANAGE_MESSAGES = 2048 * 2,
SEND_MESSAGES = 2048,
@ -35,18 +32,18 @@ export namespace PermissionTypes {
}
export type Permission = General | Text | Voice;
export type PermissionString = keyof typeof All;
export const defaultPermissions =
PermissionTypes.General.VIEW_CHANNELS
| PermissionTypes.General.CREATE_INVITE
| PermissionTypes.Text.SEND_MESSAGES
| PermissionTypes.Text.READ_MESSAGES
// | PermissionTypes.Text.ADD_REACTIONS
| PermissionTypes.Text.SEND_FILES
| PermissionTypes.Voice.CONNECT
| PermissionTypes.Voice.SPEAK;
}
export function getPermString(integer: number | string): string {
export function getPermString(integer: number | string): string {
return (typeof integer === 'string')
? Object
.entries(PermissionTypes.All)

View File

@ -59,7 +59,7 @@ export declare namespace WS {
/** Manually disconnect from the websocket; logout. */
'disconnect': any;
}
/** WS Args are what is received from the websocket. */
export interface From {
/** Called when a guild channel is created. */
@ -114,7 +114,7 @@ export declare namespace WS {
'VOICE_STATE_UPDATE': Args.VoiceStateUpdate;
'error': object;
}
export namespace Params {
export interface AddFriend {
/** Username of user (case insensitive). */
@ -146,7 +146,7 @@ export declare namespace WS {
/** ID of the channel to join. */
channelId: string;
}
export interface ChannelLeave {}
export interface ChannelLeave { }
export interface GuildCreate {
/** Name of the guild. */
name: string;
@ -237,10 +237,10 @@ export declare namespace WS {
}
export interface VoiceData {
channelId: string;
blob?: Blob;
blob?: any;
}
}
export namespace Args {
export interface ChannelCreate {
/** ID of guild that the channel is in. */

View File

@ -0,0 +1,24 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [
"ES2017"
], /* Specify library files to be included in the compilation. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
// build
"outDir": "lib",
"inlineSourceMap": true,
"declaration": true,
},
"include": [
"src/**.ts"
],
"exclude": [
"test"
]
}