diff --git a/hack/react.bzl b/hack/react.bzl index 15b482e..2b1c0f3 100644 --- a/hack/react.bzl +++ b/hack/react.bzl @@ -11,6 +11,7 @@ def _render_deps(deps = []): if has_added_grpc_deps == False: output_deps.extend([ "@npm//google-protobuf", + "@npm//@types/google-protobuf", "@npm//@improbable-eng/grpc-web", ]) has_added_grpc_deps = True diff --git a/src/common/utils/BUILD.bazel b/src/common/utils/BUILD.bazel new file mode 100644 index 0000000..3dc189e --- /dev/null +++ b/src/common/utils/BUILD.bazel @@ -0,0 +1,23 @@ +load("//:hack/react.bzl", "react_library") + +package(default_visibility = ["//visibility:public"]) + +react_library( + name = "utils", + deps = [ + "chroma-js", + "react", + "styled-components", + "//src/rpc/shared", + "@types/chroma-js", + "@types/react", + "@types/styled-components", + ], +) + +# jest_test( +# src = ":utils", +# deps = [ +# "//hack/fixtures", +# ], +# ) diff --git a/src/common/utils/ReactifyNewlines.spec.tsx b/src/common/utils/ReactifyNewlines.spec.tsx new file mode 100644 index 0000000..734fab4 --- /dev/null +++ b/src/common/utils/ReactifyNewlines.spec.tsx @@ -0,0 +1,9 @@ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { ReactifyNewlines } from './ReactifyNewlines'; + +it('renders a correct number of divs per newlines', () => { + const view = shallow({`1\n2\n3`}); + + expect(view.find('div').length).toBe(3); +}); diff --git a/src/common/utils/ReactifyNewlines.tsx b/src/common/utils/ReactifyNewlines.tsx new file mode 100644 index 0000000..ec68c49 --- /dev/null +++ b/src/common/utils/ReactifyNewlines.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; + +export const ReactifyNewlines = (props: { children: string }) => { + const textArray = props.children.split('\n'); + return ( + <> + {textArray.map((part, idx) => ( +
{part || <> }
+ ))} + + ); +}; diff --git a/src/common/utils/hasPermission.spec.ts b/src/common/utils/hasPermission.spec.ts new file mode 100644 index 0000000..9c952ed --- /dev/null +++ b/src/common/utils/hasPermission.spec.ts @@ -0,0 +1,37 @@ +import { hasPermission, permissions, hasPermissionOrAdmin } from './hasPermission'; +import { Role } from 'roleypoly/src/rpc/shared'; +import { guildRoles } from 'roleypoly/hack/fixtures/storyData'; + +const roles: Role.AsObject[] = [ + { + ...guildRoles.rolesList[0], + permissions: permissions.ADMINISTRATOR, + }, + { + ...guildRoles.rolesList[0], + permissions: + permissions.SPEAK | permissions.BAN_MEMBERS | permissions.CHANGE_NICKNAME, + }, + { + ...guildRoles.rolesList[0], + permissions: permissions.BAN_MEMBERS, + }, +]; + +it('finds a permission within a list of roles', () => { + const result = hasPermission(roles, permissions.CHANGE_NICKNAME); + + expect(result).toBe(true); +}); + +it('finds admin within a list of roles', () => { + const result = hasPermissionOrAdmin(roles, permissions.BAN_MEMBERS); + + expect(result).toBe(true); +}); + +it('does not find a permission within a list of roles without one', () => { + const result = hasPermission(roles, permissions.KICK_MEMBERS); + + expect(result).toBe(false); +}); diff --git a/src/common/utils/hasPermission.ts b/src/common/utils/hasPermission.ts new file mode 100644 index 0000000..eed6c15 --- /dev/null +++ b/src/common/utils/hasPermission.ts @@ -0,0 +1,45 @@ +import { Role } from 'roleypoly/src/rpc/shared'; + +export const hasPermission = (roles: Role.AsObject[], permission: number): boolean => { + const aggregateRoles = roles.reduce((acc, role) => acc | role.permissions, 0); + return (aggregateRoles & permission) === permission; +}; + +export const hasPermissionOrAdmin = ( + roles: Role.AsObject[], + permission: number +): boolean => hasPermission(roles, permission | permissions.ADMINISTRATOR); + +export const permissions = { + CREATE_INSTANT_INVITE: 0x1, + KICK_MEMBERS: 0x2, + BAN_MEMBERS: 0x4, + ADMINISTRATOR: 0x8, + MANAGE_CHANNELS: 0x10, + MANAGE_GUILD: 0x20, + ADD_REACTIONS: 0x40, + VIEW_AUDIT_LOG: 0x80, + VIEW_CHANNEL: 0x400, + SEND_MESSAGES: 0x800, + SEND_TTS_MESSAGES: 0x1000, + MANAGE_MESSAGES: 0x2000, + EMBED_LINKS: 0x4000, + ATTACH_FILES: 0x8000, + READ_MESSAGE_HISTORY: 0x10000, + MENTION_EVERYONE: 0x20000, + USE_EXTERNAL_EMOJIS: 0x40000, + VIEW_GUILD_INSIGHTS: 0x80000, + CONNECT: 0x100000, + SPEAK: 0x200000, + MUTE_MEMBERS: 0x400000, + DEAFEN_MEMBERS: 0x800000, + MOVE_MEMBERS: 0x1000000, + USE_VAD: 0x2000000, + PRIORITY_SPEAKER: 0x100, + STREAM: 0x200, + CHANGE_NICKNAME: 0x4000000, + MANAGE_NICKNAMES: 0x8000000, + MANAGE_ROLES: 0x10000000, + MANAGE_WEBHOOKS: 0x20000000, + MANAGE_EMOJIS: 0x40000000, +}; diff --git a/src/common/utils/protoReflection.spec.ts b/src/common/utils/protoReflection.spec.ts new file mode 100644 index 0000000..9b7050e --- /dev/null +++ b/src/common/utils/protoReflection.spec.ts @@ -0,0 +1,9 @@ +import { DiscordUser } from 'roleypoly/src/rpc/shared'; +import { user } from 'roleypoly/hack/fixtures/storyData'; +import { AsObjectToProto } from './protoReflection'; + +it('converts a RoleypolyUser.AsObject back to protobuf', () => { + const proto = AsObjectToProto(DiscordUser, user); + + expect(proto.toObject()).toMatchObject(user); +}); diff --git a/src/common/utils/protoReflection.ts b/src/common/utils/protoReflection.ts new file mode 100644 index 0000000..2c23b21 --- /dev/null +++ b/src/common/utils/protoReflection.ts @@ -0,0 +1,30 @@ +import * as pbjs from 'google-protobuf'; + +type GenericObject = T; +type ProtoFunction> = ( + value: U[keyof U] +) => void; + +export const AsObjectToProto = ( + protoClass: { new (): T }, + input: ReturnType +): GenericObject => { + const proto = new protoClass(); + const protoKeys = Object.getOwnPropertyNames((proto as any).__proto__); + + for (let inputKey in input) { + const setCallName = protoKeys.find( + (key) => `set${inputKey.toLowerCase()}` === key.toLowerCase() + ) as keyof typeof proto; + + if (!setCallName) { + continue; + } + + ((proto[setCallName] as unknown) as ProtoFunction)( + input[inputKey] + ); + } + + return proto; +}; diff --git a/src/common/utils/sortBy.spec.ts b/src/common/utils/sortBy.spec.ts new file mode 100644 index 0000000..71cc0ef --- /dev/null +++ b/src/common/utils/sortBy.spec.ts @@ -0,0 +1,48 @@ +import { sortBy } from './sortBy'; + +it('sorts an array of objects by its key', () => { + const output = sortBy( + [ + { + name: 'bbb', + }, + { + name: 'aaa', + }, + { + name: 'ddd', + }, + { + name: 'ccc', + }, + ], + 'name' + ); + + expect(output.map((v) => v.name)).toEqual(['aaa', 'bbb', 'ccc', 'ddd']); +}); + +it('sorts an array of objects by its key with a predicate', () => { + const output = sortBy( + [ + { + name: 'cc', + }, + { + name: 'bbb', + }, + { + name: 'aaaa', + }, + { + name: 'd', + }, + ], + 'name', + (a, b) => { + return a.length > b.length ? 1 : -1; + } + ); + + expect(output.map((v) => v.name)).toEqual(['d', 'cc', 'bbb', 'aaaa']); +}); diff --git a/src/common/utils/sortBy.ts b/src/common/utils/sortBy.ts new file mode 100644 index 0000000..e74ec5b --- /dev/null +++ b/src/common/utils/sortBy.ts @@ -0,0 +1,21 @@ +export const sortBy = ( + array: T[], + key: keyof T, + predicate?: (a: T[keyof T], b: T[keyof T]) => number +) => { + return array.sort((a, b) => { + if (predicate) { + return predicate(a[key], b[key]); + } + + if (a[key] === b[key]) { + return 0; + } + + if (a[key] > b[key]) { + return 1; + } + + return -1; + }); +};