Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions apps/juxtaposition-ui/src/models/communities.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ export const CommunitySchema = new Schema({
permissions: {
type: PermissionsSchema,
default: {}
},
shot_mode: {
type: String,
default: 'allow'
},
Comment thread
mrjvs marked this conversation as resolved.
Outdated
shot_extra_title_id: {
type: [String],
default: []
}
});

Expand Down
20 changes: 16 additions & 4 deletions apps/juxtaposition-ui/src/services/juxt-web/routes/admin/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,11 @@ adminRouter.post('/communities/new', upload.fields([{ name: 'browserIcon', maxCo
title_ids: z.string().trim()
.transform(v => v.replaceAll(' ', '').split(',').filter(v => v.length > 0))
.pipe(z.array(z.string().min(1))),
app_data: z.string().trim()
app_data: z.string().trim(),
shot_mode: z.enum(['allow', 'block', 'force']),
shot_extra_title_id: z.string().trim()
.transform(v => v.replaceAll(' ', '').split(',').filter(v => v.length > 0))
.pipe(z.array(z.string().min(1)))
}),
files: ['browserIcon', 'CTRbrowserHeader', 'WiiUbrowserHeader']
});
Expand Down Expand Up @@ -471,7 +475,9 @@ adminRouter.post('/communities/new', upload.fields([{ name: 'browserIcon', maxCo
community_id: communityId,
olive_community_id: communityId,
is_recommended: body.is_recommended,
app_data: body.app_data
app_data: body.app_data,
shot_mode: body.shot_mode,
shot_extra_title_id: body.shot_extra_title_id
};
const newCommunity = new COMMUNITY(document);
await newCommunity.save();
Expand Down Expand Up @@ -561,7 +567,11 @@ adminRouter.post('/communities/:id', upload.fields([{ name: 'browserIcon', maxCo
title_ids: z.string().trim()
.transform(v => v.replaceAll(' ', '').split(',').filter(v => v.length > 0))
.pipe(z.array(z.string().min(1))),
app_data: z.string().trim()
app_data: z.string().trim(),
shot_mode: z.enum(['allow', 'block', 'force']),
shot_extra_title_id: z.string().trim()
.transform(v => v.replaceAll(' ', '').split(',').filter(v => v.length > 0))
Comment thread
mrjvs marked this conversation as resolved.
Outdated
.pipe(z.array(z.string().min(1)))
}),
files: ['browserIcon', 'CTRbrowserHeader', 'WiiUbrowserHeader']
});
Expand Down Expand Up @@ -611,7 +621,9 @@ adminRouter.post('/communities/:id', upload.fields([{ name: 'browserIcon', maxCo
app_data: body.app_data,
is_recommended: body.is_recommended,
name: body.name,
description: body.description
description: body.description,
shot_mode: body.shot_mode,
shot_extra_title_id: body.shot_extra_title_id
};
const comm = await COMMUNITY.findOneAndUpdate({ olive_community_id: communityId }, { $set: document }, { upsert: true }).exec();
if (!comm) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CtrPostListView } from '@/services/juxt-web/views/ctr/postList';
import { zodFallback } from '@/util';
import { CtrNewPostPage } from '@/services/juxt-web/views/ctr/newPostView';
import { PortalNewPostPage } from '@/services/juxt-web/views/portal/newPostView';
import { getShotMode, isPostingAllowed } from '@/services/juxt-web/routes/permissions';
import type { InferSchemaType } from 'mongoose';
import type { PostListViewProps } from '@/services/juxt-web/views/web/postList';
import type { CommunityViewProps } from '@/services/juxt-web/views/web/communityView';
Expand Down Expand Up @@ -116,7 +117,7 @@ communitiesRouter.get('/:communityID/related', async function (req, res) {
});

communitiesRouter.get('/:communityID/create', async function (req, res) {
const { params } = parseReq(req, {
const { params, auth } = parseReq(req, {
params: z.object({
communityID: z.string()
})
Expand All @@ -127,11 +128,14 @@ communitiesRouter.get('/:communityID/create', async function (req, res) {
return res.sendStatus(404);
}

const shotMode = getShotMode(community, auth().paramPackData);

const props = {
id: community.olive_community_id,
name: community.name,
url: `/posts/new`,
show: 'post'
show: 'post',
shotMode
};
res.jsxForDirectory({
ctr: <CtrNewPostPage {...props} />,
Expand All @@ -140,7 +144,7 @@ communitiesRouter.get('/:communityID/create', async function (req, res) {
});

communitiesRouter.get('/:communityID/:type', async function (req, res) {
const { query, params, auth } = parseReq(req, {
const { query, params, hasAuth, auth } = parseReq(req, {
params: z.object({
communityID: z.string(),
type: z.string()
Expand Down Expand Up @@ -172,11 +176,7 @@ communitiesRouter.get('/:communityID/:type', async function (req, res) {
};
await community.save();
}
const canPost = (
(community.permissions.open && community.type < 2) ||
(community.admins && community.admins.indexOf(auth().pid) !== -1) ||
(auth().user.accessLevel >= community.permissions.minimum_new_post_access_level)
) && userSettings.account_status === 0;
const canPost = hasAuth() && userSettings !== null && isPostingAllowed(community, userSettings, null, auth().user);
const isUserFollowing = userContent.followed_communities.includes(community.olive_community_id);

const subCommunities = await database.getSubCommunities(community.olive_community_id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,8 @@ messagesRouter.get('/:message_id/create', async function (req, res) {
pid: partner.pid,
messagePid: partner.pid,
url: `/friend_messages/new`,
show: 'message-page'
show: 'message-page',
shotMode: 'allow'
};
res.jsxForDirectory({
ctr: <CtrNewPostPage {...props} />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ import { CtrNewPostPage } from '@/services/juxt-web/views/ctr/newPostView';
import { PortalNewPostPage } from '@/services/juxt-web/views/portal/newPostView';
import { PortalReportPostPage } from '@/services/juxt-web/views/portal/reportPostView';
import { CtrReportPostPage } from '@/services/juxt-web/views/ctr/reportPostView';
import { getShotMode, isPostingAllowed } from '@/services/juxt-web/routes/permissions';
import type { Request, Response } from 'express';
import type { InferSchemaType } from 'mongoose';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { PaintingUrls } from '@/images';
import type { PostPageViewProps } from '@/services/juxt-web/views/web/postPageView';
import type { PostSchema } from '@/models/post';
import type { CommunitySchema } from '@/models/communities';
import type { HydratedSettingsDocument } from '@/models/settings';
import type { ContentSchema } from '@/models/content';
const upload = multer({ dest: 'uploads/' });
Expand Down Expand Up @@ -176,11 +174,7 @@ postsRouter.get('/:post_id', async function (req, res) {
// increase limit for post replies since there's no pagination yet
const replies = (await getPostsByParentId(maybeTokens, post.id, 0, 500))?.items ?? [];
const postPNID = await getUserAccountData(post.pid);
const canPost = hasAuth() && (
(community.permissions.open && community.type < 2) ||
(community.admins && community.admins.indexOf(auth().pid) !== -1) ||
(auth().user.accessLevel >= community.permissions.minimum_new_comment_access_level)
) && userSettings?.account_status === 0;
const canPost = hasAuth() && userSettings !== null && isPostingAllowed(community, userSettings, post, auth().user);

const props: PostPageViewProps = {
community,
Expand Down Expand Up @@ -264,11 +258,19 @@ postsRouter.get('/:post_id/create', async function (req, res) {
return res.sendStatus(404);
}

const community = await database.getCommunityByID(parent.community_id);
if (!community) {
return res.sendStatus(404);
}

const shotMode = getShotMode(community, auth().paramPackData);

const props = {
id: parent.community_id,
pid: parent.pid,
url: `/posts/${parent.id}/new`,
show: 'post'
show: 'post',
shotMode
};
res.jsxForDirectory({
ctr: <CtrNewPostPage {...props} />,
Expand Down Expand Up @@ -332,32 +334,6 @@ postsRouter.post('/:post_id/report', upload.none(), async function (req, res) {
return res.redirect(`/posts/${post.id}`);
});

function canPost(community: InferSchemaType<typeof CommunitySchema>, userSettings: HydratedSettingsDocument, parentPost: InferSchemaType<typeof PostSchema> | null, user: GetUserDataResponse): boolean {
const isReply = !!parentPost;
const isPublicPostableCommunity = community.type >= 0 && community.type < 2;
const isOpenCommunity = community.permissions.open;

const isCommunityAdmin = (community.admins ?? []).includes(user.pid);
const isUserLimitedFromPosting = userSettings.account_status !== 0;
const hasAccessLevelRequirement = isReply
? user.accessLevel >= community.permissions.minimum_new_comment_access_level
: user.accessLevel >= community.permissions.minimum_new_post_access_level;

if (isUserLimitedFromPosting) {
return false;
}

if (isCommunityAdmin) {
return true; // admins can always post (if not limited)
}

if (!hasAccessLevelRequirement) {
return false;
}

return isReply ? isOpenCommunity : isPublicPostableCommunity;
}

async function newPost(req: Request, res: Response): Promise<void> {
const { params, body, auth } = parseReq(req, {
params: z.object({
Expand Down Expand Up @@ -404,7 +380,7 @@ async function newPost(req: Request, res: Response): Promise<void> {
return res.redirect('/titles/show');
}

if (!canPost(community, userSettings, parentPost, req.user)) {
if (!isPostingAllowed(community, userSettings, parentPost, req.user)) {
res.status(403);
return res.redirect(`/titles/${community.olive_community_id}/new`);
}
Expand All @@ -428,7 +404,7 @@ async function newPost(req: Request, res: Response): Promise<void> {
}
}
let screenshots = null;
if (body.screenshot) {
if (body.screenshot && getShotMode(community, auth().paramPackData) !== 'block') {
screenshots = await uploadScreenshot({
blob: body.screenshot,
pid: auth().pid,
Expand Down
55 changes: 55 additions & 0 deletions apps/juxtaposition-ui/src/services/juxt-web/routes/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { InferSchemaType } from 'mongoose';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { CommunitySchema } from '@/models/communities';
import type { PostSchema } from '@/models/post';
import type { HydratedSettingsDocument } from '@/models/settings';
import type { PostDto } from '@/api/post';
import type { ParamPack } from '@/types/common/param-pack';

export function isPostingAllowed(community: InferSchemaType<typeof CommunitySchema>, userSettings: HydratedSettingsDocument, parentPost: InferSchemaType<typeof PostSchema> | PostDto | null, user: GetUserDataResponse): boolean {
const isReply = !!parentPost;
const isPublicPostableCommunity = community.type >= 0 && community.type < 2;
const isOpenCommunity = community.permissions.open;

const isCommunityAdmin = (community.admins ?? []).includes(user.pid);
const isUserLimitedFromPosting = userSettings.account_status !== 0;
const hasAccessLevelRequirement = isReply
? user.accessLevel >= community.permissions.minimum_new_comment_access_level
: user.accessLevel >= community.permissions.minimum_new_post_access_level;

if (isUserLimitedFromPosting) {
return false;
}

if (isCommunityAdmin) {
return true; // admins can always post (if not limited)
}

if (!hasAccessLevelRequirement) {
return false;
}

return isReply ? isOpenCommunity : isPublicPostableCommunity;
}

export type ShotMode = 'allow' | 'block' | 'force';
const shotModes = ['allow', 'block', 'force'];

export function getShotMode(community: InferSchemaType<typeof CommunitySchema>, pack: ParamPack | null): ShotMode {
if (pack === null) {
return 'block';
}

// Shots only on matching communities
if (!community.title_id.includes(pack.title_id) &&
!community.shot_extra_title_id?.includes(pack.title_id)) {
return 'block';
}

// Check for bad community schema
if (!shotModes.includes(community.shot_mode ?? '')) {
return 'allow'; // default
}

return community.shot_mode as ShotMode; // type check above
}
Comment thread
mrjvs marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import cx from 'classnames';
import { useDatasetProps } from '@/services/juxt-web/views/common/hooks/useDataset';
import type { ReactNode } from 'react';
import type { DatasetProps } from '@/services/juxt-web/views/common/hooks/useDataset';

export type CtrTabViewProps = {
export type CtrTabViewProps = DatasetProps & {
// Shared name for all tabs.
name: string;
// Form value for this specific tab.
Expand All @@ -16,10 +18,11 @@ export type CtrTabViewProps = {

// Client-side tab control
export function CtrTabView(props: CtrTabViewProps): ReactNode {
const dataset = useDatasetProps(props);
const selected = { selected: props.default };
return (
<>
<li className={cx('ctab', selected)} data-ctab="1">
<li className={cx('ctab', selected)} data-ctab="1" {...dataset}>
<div className={cx('sprite', 'centred', props.sprite, selected)}></div>
<input
type="radio"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ export function CtrNewPostView(props: NewPostViewProps): ReactNode {
placeholder="Share your thoughts in a post to a community or to your followers."
/>
</CtrTabView>
<CtrTabView name="_post_type" value="shot" sprite="sp-shot-input">
<div id="shot-msg">Screenshots are not ready yet. Check back soon!</div>
</CtrTabView>
{props.shotMode !== 'block'
? (
<CtrTabView name="_post_type" value="shot" sprite="sp-shot-input" data-shot-mode={props.shotMode}>
<div id="shot-msg">Screenshots are not ready yet. Check back soon!</div>
</CtrTabView>
)
: null }

<CtrTabView name="_post_type" value="painting" sprite="sp-memo-input">
<img id="memo-img-input" src="" />
<input type="hidden" name="painting" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,33 @@ export function PortalNewPostView(props: NewPostViewProps): ReactNode {
))}
</ul>
</div>
<div className="image-selector dropdown">
<label data-toggle="dropdown" className="dropdown-toggle" data-sound="SE_WAVE_BALLOON_OPEN">
<img className="preview-image" src="/assets/portal/images/add-post-no-image.png" />
<input type="checkbox" id="screenshot-toggle" name="_screenshot_value" />
<div className="image-selector-window dropdown-menu">
<menu className="screenshot-menu">
<li className="image-wrapper">
<input type="radio" name="_screenshot_value" value="top" defaultChecked data-sound="" evt-click="chooseScreenShot(0)" />
<img id="top-screen" src="" className="capture-image" />
</li>
<li className="image-wrapper">
<input type="radio" name="_screenshot_value" value="bottom" data-sound="" evt-click="chooseScreenShot(1)" />
<img id="bottom-screen" src="" className="capture-image" />
</li>
<li className="button">
<input type="radio" name="_screenshot_value" value="none" data-sound="" evt-click="chooseScreenShot()" />
No Screenshot
</li>
</menu>
</div>
</label>
<input id="screenshot-value" type="hidden" name="screenshot" value="" />
</div>
{props.shotMode !== 'block'
? (
<div className="image-selector dropdown">
<label data-toggle="dropdown" className="dropdown-toggle" data-sound="SE_WAVE_BALLOON_OPEN">
<img className="preview-image" src="/assets/portal/images/add-post-no-image.png" />
<input type="checkbox" id="screenshot-toggle" name="_screenshot_value" />
<div className="image-selector-window dropdown-menu">
<menu className="screenshot-menu">
<li className="image-wrapper">
<input type="radio" name="_screenshot_value" value="top" defaultChecked data-sound="" evt-click="chooseScreenShot(0)" />
<img id="top-screen" src="" className="capture-image" />
</li>
<li className="image-wrapper">
<input type="radio" name="_screenshot_value" value="bottom" data-sound="" evt-click="chooseScreenShot(1)" />
<img id="bottom-screen" src="" className="capture-image" />
</li>
<li className="button">
<input type="radio" name="_screenshot_value" value="none" data-sound="" evt-click="chooseScreenShot()" />
No Screenshot
</li>
</menu>
</div>
</label>
<input id="screenshot-value" type="hidden" name="screenshot" value="" />
</div>
)
: null }
<div className="textarea-container textarea-with-menu active-text">
<menu className="textarea-menu">
<li className="textarea-menu-text">
Expand Down
Loading