Skip to content

Commit 98703d4

Browse files
authored
Merge pull request #33 from velog-io/feature/post-api
Feature/post api
2 parents e2c422c + bff7f08 commit 98703d4

File tree

49 files changed

+2126
-239
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2126
-239
lines changed

packages/velog-cron/src/common/plugins/globals/cronPlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const cronPlugin: FastifyPluginCallback = async (fastfiy, opts, done) => {
3434
name: 'generate feeds in every 1 minute',
3535
cronTime: '*/1 * * * *', // every 1 minute
3636
jobService: generateFeedJob,
37+
isImmediateExecute: true,
3738
},
3839
{
3940
name: 'posts score calculation in every 5 minutes',

packages/velog-prisma/scripts/deploy.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ function getMessage(migrationDiff: string[]): string {
5757
const question = `🤔 Are you sure?`
5858
return [
5959
title,
60-
...migrationDiff.map((filename) => ` - ${filename}\n\n${getMigrationSummary(filename)}`),
61-
'',
60+
...migrationDiff.map((filename) => ` - ${filename}\n${getMigrationSummary(filename)}`),
6261
question,
6362
].join('\n')
6463
}
@@ -72,10 +71,12 @@ function getMigrationSummary(filename: string) {
7271
)
7372

7473
const sql = fs.readFileSync(filepath, { encoding: 'utf-8' })
74+
const lines = sql.split('\n')
75+
7576
const target = ['ALTER', 'CREATE', 'DROP']
76-
return sql
77-
.split('\n')
77+
return lines
7878
.filter((line) => target.some((str) => line.includes(str)))
7979
.map((v) => ` - ${v.replace(/[{}\[\]()<>;]+/g, '').toLowerCase()}`)
80+
.map((v, i, arr) => (i === arr.length - 1 ? `${v}\n` : v))
8081
.join('\n')
8182
}

packages/velog-scripts/env/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ DATABASE_URL=
22
DISCORD_BOT_TOKEN=
33
DISCORD_PRIVATE_POSTS_CHANNEL_ID=
44
REDIS_HOST=
5-
SPAM_ACCOUNT_DISPLAY_NAME=
5+
SPAM_ACCOUNT_DISPLAY_NAME=
6+
BANNED_KEYWORDS=

packages/velog-scripts/env/env.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const env = z.object({
1515
discordPrivatePostsChannelId: z.string(),
1616
redisHost: z.string(),
1717
restorePostsUsername: z.string().optional(),
18-
bannedKeywords: z.array(z.string()).min(1),
18+
bannedKeywords: z.array(z.string()),
1919
})
2020

2121
export const ENV = env.parse({
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { DbService } from '../db/DbService.mjs'
2+
import { injectable, singleton } from 'tsyringe'
3+
4+
interface Service {
5+
addBlockList: (username: string) => Promise<void>
6+
readBlockList: () => Promise<string[]>
7+
}
8+
9+
@injectable()
10+
@singleton()
11+
export class BlockListService implements Service {
12+
constructor(private readonly db: DbService) {}
13+
public async addBlockList(username: string) {
14+
await this.db.dynamicConfigItem.create({
15+
data: {
16+
value: username,
17+
type: 'username',
18+
},
19+
})
20+
}
21+
public async readBlockList() {
22+
const blockList = await this.db.dynamicConfigItem.findMany({
23+
where: {
24+
type: 'username',
25+
},
26+
})
27+
return blockList.map((item) => item.value)
28+
}
29+
}

packages/velog-scripts/scripts/privatePostsOfSpamAccount.mts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@ import { container, injectable } from 'tsyringe'
55
import inquirer from 'inquirer'
66
import { ENV } from '../env/env.mjs'
77
import { DiscordService } from '../lib/discord/DiscordService.mjs'
8-
import { RedisService } from '../lib/redis/RedisService.mjs'
8+
import { BlockListService } from '../lib/blockList/BlockListService.mjs'
99

1010
interface IRunner {}
1111

1212
@injectable()
1313
class Runner implements IRunner {
14-
blockList!: string[]
1514
constructor(
1615
private readonly db: DbService,
1716
private readonly discord: DiscordService,
18-
private readonly redis: RedisService,
17+
private readonly blockList: BlockListService,
1918
) {}
2019
public async run(names: string[]) {
2120
await this.init()
@@ -26,8 +25,12 @@ class Runner implements IRunner {
2625
const user = await this.findUsersByUsername(username)
2726
const posts = await this.findWritenPostsByUserId(user.id)
2827

29-
// add block list
30-
await this.redis.addBlockList(username)
28+
const blockList = await this.blockList.readBlockList()
29+
30+
if (blockList.includes(username)) {
31+
console.log(`${username} 유저는 이미 등록되어 있습니다.`)
32+
continue
33+
}
3134

3235
if (posts.length === 0) {
3336
console.log(`${user.username} 유저의 비공개 처리 할 게시글이 없습니다.`)
@@ -41,9 +44,12 @@ class Runner implements IRunner {
4144
user.profile?.display_name || null,
4245
)
4346

47+
if (!askResult.is_set_private) continue
48+
4449
const postIds = posts.map(({ id }) => id!)
50+
// add block list
51+
await this.blockList.addBlockList(username)
4552

46-
if (!askResult.is_set_private) continue
4753
// set private = true
4854
await this.setIsPrivatePost(postIds)
4955

@@ -62,21 +68,24 @@ class Runner implements IRunner {
6268
}
6369

6470
if (handledUser.length === 0) {
65-
console.log('비공개 게시글 처리된 유저가 존재하지 않습니다.')
71+
console.log('비공개 게시글로 처리된 유저가 존재하지 않습니다.')
6672
process.exit(0)
6773
}
6874

6975
try {
70-
const result = await this.discord.sendMessage(
71-
ENV.discordPrivatePostsChannelId,
72-
JSON.stringify({
73-
title: '해당 유저의 글들이 비공개 처리 되었습니다.',
74-
userInfo: handledUser,
75-
}),
76-
)
77-
78-
handledUser.map((u) => console.log(`${u.username} 유저가 처리 되었습니다.`))
79-
console.log(result)
76+
const promises = handledUser.map(async (userInfo) => {
77+
await this.discord.sendMessage(
78+
ENV.discordPrivatePostsChannelId,
79+
JSON.stringify({
80+
title: '해당 유저의 글들이 비공개 처리 되었습니다.',
81+
userInfo: userInfo,
82+
}),
83+
)
84+
85+
console.log(`${userInfo.username} 유저가 처리 되었습니다.`)
86+
})
87+
88+
await Promise.all(promises)
8089
process.exit(0)
8190
} catch (error) {
8291
console.log(error)
@@ -85,9 +94,7 @@ class Runner implements IRunner {
8594
private async init() {
8695
try {
8796
await this.discord.connection()
88-
await this.redis.connection()
89-
90-
this.blockList = await this.redis.readBlockList()
97+
await this.db.$connect()
9198
} catch (error) {
9299
throw error
93100
}
@@ -130,7 +137,8 @@ class Runner implements IRunner {
130137
username: string,
131138
displayName: string | null,
132139
): Promise<AskDeletePostsResult> {
133-
if (this.blockList.includes(username)) {
140+
const blockedList = await this.blockList.readBlockList()
141+
if (blockedList.includes(username)) {
134142
return {
135143
posts,
136144
is_set_private: true,

packages/velog-server/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"main": "main.ts",
1616
"type": "module",
1717
"scripts": {
18-
"dev": "nodemon --watch './**/*.ts' --exec 'node --loader ts-paths-esm-loader' src/main.ts | pino-pretty",
18+
"dev": "nodemon --watch './**/*.ts' --exec 'node --loader ts-paths-esm-loader/transpile-only' src/main.ts | pino-pretty",
1919
"stage": "pnpm ssm pull -e stage && NODE_ENV=production pnpm start",
2020
"prod": "pnpm ssm pull -e production && NODE_ENV=production pnpm start",
2121
"build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json",
@@ -55,6 +55,7 @@
5555
"fastify": "^4.18.0",
5656
"fastify-multer": "^2.0.3",
5757
"fastify-plugin": "^4.5.1",
58+
"geoip-country": "^4.2.68",
5859
"googleapis": "^124.0.0",
5960
"graphql": "^16.7.1",
6061
"graphql-scalars": "^1.22.2",
@@ -79,6 +80,7 @@
7980
"tsx": "^4.6.2",
8081
"tsyringe": "^4.7.0",
8182
"uuid": "^9.0.1",
83+
"velog-server": "link:",
8284
"zod": "^3.21.4"
8385
},
8486
"devDependencies": {
@@ -89,6 +91,7 @@
8991
"@graphql-codegen/typescript-operations": "^4.0.0",
9092
"@graphql-codegen/typescript-resolvers": "^4.0.0",
9193
"@types/backblaze-b2": "^1.5.2",
94+
"@types/geoip-country": "^4.0.2",
9295
"@types/inquirer": "^9.0.7",
9396
"@types/jest": "^29.5.2",
9497
"@types/jsonwebtoken": "^9.0.2",
@@ -106,6 +109,7 @@
106109
"eslint-config-prettier": "^9.0.0",
107110
"eslint-plugin-prettier": "^5.0.0",
108111
"jest": "^29.6.4",
112+
"jest-mock-axios": "^4.7.3",
109113
"jest-mock-extended": "^3.0.5",
110114
"json-schema-to-ts": "^2.9.2",
111115
"nodemon": "^2.0.22",

packages/velog-server/scripts/ssm/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@ class ParameterService {
9191
const command = new PutParameterCommand(input)
9292
const response = await this.client.send(command)
9393
console.info(`Parameter upload successful! path: ${name}, version: ${response.Version}`)
94-
} catch (_) {}
94+
} catch (error) {
95+
console.error(error)
96+
}
9597
}
9698
}
9799

packages/velog-server/src/common/plugins/encapsulated/authGuardPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import fp from 'fastify-plugin'
55
const authGuardPlugin: FastifyPluginAsync = async (fastify) => {
66
fastify.addHook('preHandler', async (request) => {
77
if (!request.user) {
8-
throw new UnauthorizedError('Not Logged In')
8+
throw new UnauthorizedError('Not logged in')
99
}
1010
})
1111
}

packages/velog-server/src/common/plugins/global/ipaddrPlugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ const ipaddrPlugin: FastifyPluginAsync = async (fastify) => {
55
fastify.addHook('preHandler', (request, reply, done) => {
66
const fromCdnIp = request.headers['gcdn-client-ip']
77
const graphCdnAddress = Array.isArray(fromCdnIp) ? fromCdnIp[0] : fromCdnIp
8-
request.ipaddr = graphCdnAddress || request.ips?.slice(-1)[0] || request.ip
8+
const ipaddr = graphCdnAddress || request.ips?.slice(-1)[0] || request.ip
9+
request.ipaddr = ipaddr
910
done()
1011
})
1112
}

packages/velog-server/src/common/plugins/global/mercuriusPlugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const mercuriusPlugin: FastifyPluginAsync = async (fastify) => {
3232
errors: errors?.map((error) => ({ name: error.name, message: error.message })),
3333
}
3434
if (ENV.appEnv === 'development') {
35+
console.log('errorHandler')
3536
request.log.error(request, 'errorHandler')
3637
} else {
3738
const discord = container.resolve(DiscordService)
@@ -50,6 +51,7 @@ const mercuriusPlugin: FastifyPluginAsync = async (fastify) => {
5051
const e = execution.errors?.[0]?.originalError
5152

5253
if (!isHttpError(e)) {
54+
console.log('send!')
5355
;(ctx as any).request?.log?.error(execution, 'errorFormatter')
5456
const discord = container.resolve(DiscordService)
5557
discord.sendMessage(

packages/velog-server/src/env.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { existsSync } from 'fs'
33
import { z } from 'zod'
44
import { fileURLToPath } from 'url'
55
import { dirname, join } from 'path'
6+
import { container } from 'tsyringe'
7+
import { DbService } from '@lib/db/DbService.js'
68

79
type DockerEnv = 'development' | 'stage' | 'production'
810
type AppEnvironment = 'development' | 'production'
@@ -14,9 +16,9 @@ const envFiles: EnvFiles = {
1416
stage: '.env.stage',
1517
}
1618

17-
if (!process.env.DOCKER_ENV && process.env.NODE_ENV !== 'development') {
19+
if (!process.env.DOCKER_ENV && process.env.NODE_ENV !== undefined) {
1820
console.error(
19-
'Development environment was initiated despite the absence of the Docker environment.',
21+
'Development environment was initiated, despite the absence of the Docker environment.',
2022
)
2123
}
2224

@@ -72,6 +74,7 @@ const env = z.object({
7274
codenaryApiKey: z.string(),
7375
codenaryCallback: z.string(),
7476
redisHost: z.string(),
77+
redisPort: z.number(),
7578
slackUrl: z.string(),
7679
slackImage: z.string(),
7780
blacklistUsername: z.array(z.string()),
@@ -80,8 +83,15 @@ const env = z.object({
8083
adFreeWritersUsername: z.array(z.string()),
8184
discordBotToken: z.string(),
8285
discordErrorChannel: z.string(),
86+
discordSpamChannel: z.string(),
87+
turnstileSecretKey: z.string(),
88+
bannedKeywords: z.array(z.string()),
89+
bannedAltKeywords: z.array(z.string()),
90+
graphcdnToken: z.string(),
8391
})
8492

93+
const { bannedKeywords, bannedAltKeywords } = await readEnvFromDatabase()
94+
8595
export const ENV = env.parse({
8696
dockerEnv,
8797
appEnv,
@@ -110,6 +120,7 @@ export const ENV = env.parse({
110120
codenaryApiKey: process.env.CODENARY_API_KEY,
111121
codenaryCallback: process.env.CODENARY_CALLBACK,
112122
redisHost: process.env.REDIS_HOST,
123+
redisPort: Number(process.env.REDIS_PORT) || 6379,
113124
slackUrl: `https://hooks.slack.com/services/${process.env.SLACK_TOKEN}`,
114125
slackImage: process.env.SLACK_IMAGE,
115126
blacklistUsername: (process.env.BLACKLIST_USERNAME ?? '').split(','),
@@ -118,4 +129,19 @@ export const ENV = env.parse({
118129
adFreeWritersUsername: process.env.AD_FREE_WRITERS_USERNAME?.split(','),
119130
discordBotToken: process.env.DISCORD_BOT_TOKEN,
120131
discordErrorChannel: process.env.DISCORD_ERROR_CHANNEL,
132+
discordSpamChannel: process.env.DISCORD_SPAM_CHANNEL,
133+
turnstileSecretKey: process.env.TURNSTILE_SECRET_KEY,
134+
bannedKeywords: bannedKeywords,
135+
bannedAltKeywords: bannedAltKeywords,
136+
graphcdnToken: process.env.GRAPHCDN_TOKEN,
121137
})
138+
139+
async function readEnvFromDatabase() {
140+
const db = container.resolve(DbService)
141+
const items = await db.dynamicConfigItem.findMany()
142+
143+
return {
144+
bannedKeywords: items.filter((item) => item.type === 'banned').map((item) => item.value),
145+
bannedAltKeywords: items.filter((item) => item.type === 'bannedAlt').map((item) => item.value),
146+
}
147+
}

0 commit comments

Comments
 (0)