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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { alterEnumString } from '../utils.js'

const table = 'comment'

export const up = async (knex) => {
await knex.raw(
alterEnumString(table, 'type', [
'article',
'circle_discussion',
'circle_broadcast',
'moment',
'campaign_discussion',
])
)
}

export const down = async (knex) => {
await knex.raw(
alterEnumString(table, 'type', [
'article',
'circle_discussion',
'circle_broadcast',
'moment',
])
)
}
8 changes: 8 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,12 @@ type WritingChallenge implements Node & Campaign & Channel {
state: CampaignState!
participants(input: CampaignParticipantsInput!): CampaignParticipantConnection!
articles(input: CampaignArticlesInput!): CampaignArticleConnection!

"""Comments made by campaign participants (public to read)."""
discussion(input: CommentsInput!): CommentConnection!

"""Discussion (include replies) count of this campaign."""
discussionCount: Int!
application: CampaignApplication
featuredDescription(input: TranslationArgs): String!
organizers: [User!]!
Expand Down Expand Up @@ -1724,6 +1730,7 @@ input CommentInput {
articleId: ID
circleId: ID
momentId: ID
campaignId: ID
}

input CommentCommentsInput {
Expand Down Expand Up @@ -1914,6 +1921,7 @@ enum CommentType {
circleDiscussion
circleBroadcast
moment
campaignDiscussion
}

"""
Expand Down
4 changes: 4 additions & 0 deletions src/common/enums/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const COMMENT_TYPE = {
circleDiscussion: 'circle_discussion',
circleBroadcast: 'circle_broadcast',
moment: 'moment',
campaignDiscussion: 'campaign_discussion',
} as const

export const COMMENT_TYPES_REVERSED = Object.fromEntries(
Expand Down Expand Up @@ -251,6 +252,9 @@ export const MAX_ARTICLE_CONTENT_REVISION_LENGTH = 50

export const MAX_ARTICLE_COMMENT_LENGTH = 1200

// campaign discussion comment length cap (matches moment / 短動態 = 240)
export const MAX_CAMPAIGN_COMMENT_LENGTH = 240

export const MAX_PINNED_WORKS_LIMIT = 3

export const LATEST_WORKS_NUM = 4
Expand Down
6 changes: 6 additions & 0 deletions src/connectors/campaignService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ export class CampaignService {
where: { userId, campaignId },
})

// whether the user has successfully applied to (i.e. participates in) the campaign
public isParticipant = async (campaignId: string, userId: string) => {
const application = await this.getApplication(campaignId, userId)
return application?.state === CAMPAIGN_USER_STATE.succeeded
}

public findAndCountAll = async (
{ skip, take }: { skip: number; take: number },
{
Expand Down
42 changes: 34 additions & 8 deletions src/connectors/commentService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
Article,
Campaign,
Circle,
Comment,
CommunityWatchAction,
Expand Down Expand Up @@ -32,6 +33,7 @@ import {
import { v4 } from 'uuid'

import { BaseService } from './baseService.js'
import { CampaignService } from './campaignService.js'
import { NotificationService } from './notification/notificationService.js'
import { PaymentService } from './paymentService.js'
import { SpamDetector } from './spamDetector.js'
Expand Down Expand Up @@ -644,25 +646,31 @@ export class CommentService extends BaseService<Comment> {
// check target
let article: Article
let circle: Circle | undefined = undefined
let targetAuthorId: string
let campaign: Campaign | undefined = undefined
let targetAuthorId: string | undefined
if (comment.type === COMMENT_TYPE.article) {
article = await this.models.articleIdLoader.load(comment.targetId)
targetAuthorId = article.authorId
} else if (comment.type === COMMENT_TYPE.moment) {
const moment = await this.models.momentIdLoader.load(comment.targetId)
targetAuthorId = moment.authorId
} else if (comment.type === COMMENT_TYPE.campaignDiscussion) {
// campaign discussion has no single target author
campaign = await this.models.campaignIdLoader.load(comment.targetId)
} else {
circle = await this.models.circleIdLoader.load(comment.targetId)
targetAuthorId = circle.owner
}

const userService = new UserService(this.connections)
const isBlocked = await userService.blocked({
userId: targetAuthorId,
targetId: user.id,
})
if (isBlocked) {
throw new ForbiddenError('blocked user has no permission')
if (targetAuthorId) {
const userService = new UserService(this.connections)
const isBlocked = await userService.blocked({
userId: targetAuthorId,
targetId: user.id,
})
if (isBlocked) {
throw new ForbiddenError('blocked user has no permission')
}
}

// check permission
Expand All @@ -680,6 +688,24 @@ export class CommentService extends BaseService<Comment> {
}
}

if (campaign) {
const campaignService = new CampaignService(this.connections)
const isParticipant = await campaignService.isParticipant(
campaign.id,
user.id
)
const isOrganizer =
campaign.creatorId === user.id ||
(campaign.organizerIds ?? []).includes(user.id) ||
(campaign.managerIds ?? []).includes(user.id)

if (!isParticipant && !isOrganizer) {
throw new ForbiddenError(
'only campaign participants have the permission'
)
}
}

// check is voted before
const voted = await this.findVotesByUserId({
userId: user.id,
Expand Down
17 changes: 17 additions & 0 deletions src/definitions/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1432,6 +1432,7 @@ export type GQLCommentEdge = {

export type GQLCommentInput = {
articleId?: InputMaybe<Scalars['ID']['input']>
campaignId?: InputMaybe<Scalars['ID']['input']>
circleId?: InputMaybe<Scalars['ID']['input']>
content: Scalars['String']['input']
mentions?: InputMaybe<Array<Scalars['ID']['input']>>
Expand Down Expand Up @@ -1472,6 +1473,7 @@ export type GQLCommentState = 'active' | 'archived' | 'banned' | 'collapsed'

export type GQLCommentType =
| 'article'
| 'campaignDiscussion'
| 'circleBroadcast'
| 'circleDiscussion'
| 'moment'
Expand Down Expand Up @@ -5111,6 +5113,10 @@ export type GQLWritingChallenge = GQLCampaign &
channelEnabled: Scalars['Boolean']['output']
cover?: Maybe<Scalars['String']['output']>
description?: Maybe<Scalars['String']['output']>
/** Comments made by campaign participants (public to read). */
discussion: GQLCommentConnection
/** Discussion (include replies) count of this campaign. */
discussionCount: Scalars['Int']['output']
featuredDescription: Scalars['String']['output']
id: Scalars['ID']['output']
isManager: Scalars['Boolean']['output']
Expand All @@ -5136,6 +5142,10 @@ export type GQLWritingChallengeDescriptionArgs = {
input?: InputMaybe<GQLTranslationArgs>
}

export type GQLWritingChallengeDiscussionArgs = {
input: GQLCommentsInput
}

export type GQLWritingChallengeFeaturedDescriptionArgs = {
input?: InputMaybe<GQLTranslationArgs>
}
Expand Down Expand Up @@ -11929,6 +11939,13 @@ export type GQLWritingChallengeResolvers<
ContextType,
Partial<GQLWritingChallengeDescriptionArgs>
>
discussion?: Resolver<
GQLResolversTypes['CommentConnection'],
ParentType,
ContextType,
RequireFields<GQLWritingChallengeDiscussionArgs, 'input'>
>
discussionCount?: Resolver<GQLResolversTypes['Int'], ParentType, ContextType>
featuredDescription?: Resolver<
GQLResolversTypes['String'],
ParentType,
Expand Down
10 changes: 10 additions & 0 deletions src/mutations/comment/deleteComment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ const resolver: GQLMutationResolvers['deleteComment'] = async (
? [(await atomService.momentIdLoader.load(comment.targetId)).authorId]
: []),
]
if (comment.type === COMMENT_TYPE.campaignDiscussion) {
const campaign = await atomService.campaignIdLoader.load(comment.targetId)
authorized.push(
campaign.creatorId,
...(campaign.organizerIds ?? []),
...(campaign.managerIds ?? [])
)
}
if (!authorized.includes(viewer.id)) {
throw new ForbiddenError('viewer has no permission')
}
Expand All @@ -65,6 +73,8 @@ const resolver: GQLMutationResolvers['deleteComment'] = async (
? NODE_TYPES.Article
: comment.type === COMMENT_TYPE.moment
? NODE_TYPES.Moment
: comment.type === COMMENT_TYPE.campaignDiscussion
? NODE_TYPES.Campaign
: NODE_TYPES.Circle,
},
redis: connections.redis,
Expand Down
Loading
Loading