Resolve #5963
This commit is contained in:
		
							parent
							
								
									aa2c8d101e
								
							
						
					
					
						commit
						a54de07260
					
				
					 16 changed files with 247 additions and 5 deletions
				
			
		|  | @ -412,6 +412,9 @@ dayOverDayChanges: "前日比" | ||||||
| accessibility: "アクセシビリティ" | accessibility: "アクセシビリティ" | ||||||
| clinetSettings: "クライアント設定" | clinetSettings: "クライアント設定" | ||||||
| accountSettings: "アカウント設定" | accountSettings: "アカウント設定" | ||||||
|  | promotion: "プロモーション" | ||||||
|  | promote: "プロモート" | ||||||
|  | numberOfDays: "日数" | ||||||
| 
 | 
 | ||||||
| _ago: | _ago: | ||||||
|   unknown: "謎" |   unknown: "謎" | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								migration/1581979837262-promo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								migration/1581979837262-promo.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class promo1581979837262 implements MigrationInterface { | ||||||
|  |     name = 'promo1581979837262' | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`CREATE TABLE "promo_note" ("noteId" character varying(32) NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "REL_e263909ca4fe5d57f8d4230dd5" UNIQUE ("noteId"), CONSTRAINT "PK_e263909ca4fe5d57f8d4230dd5c" PRIMARY KEY ("noteId"))`, undefined); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_83f0862e9bae44af52ced7099e" ON "promo_note" ("userId") `, undefined); | ||||||
|  |         await queryRunner.query(`CREATE TABLE "promo_read" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_61917c1541002422b703318b7c9" PRIMARY KEY ("id"))`, undefined); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_9657d55550c3d37bfafaf7d4b0" ON "promo_read" ("userId") `, undefined); | ||||||
|  |         await queryRunner.query(`CREATE UNIQUE INDEX "IDX_2882b8a1a07c7d281a98b6db16" ON "promo_read" ("userId", "noteId") `, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "promo_read" ADD CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_a46a1a603ecee695d7db26da5f4"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "promo_read" DROP CONSTRAINT "FK_9657d55550c3d37bfafaf7d4b05"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "FK_e263909ca4fe5d57f8d4230dd5c"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_2882b8a1a07c7d281a98b6db16"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_9657d55550c3d37bfafaf7d4b0"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP TABLE "promo_read"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_83f0862e9bae44af52ced7099e"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP TABLE "promo_note"`, undefined); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| <sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed"> | <sequential-entrance class="sqadhkmv" ref="list" :direction="direction" :reversed="reversed"> | ||||||
| 	<template v-for="(item, i) in items"> | 	<template v-for="(item, i) in items"> | ||||||
| 		<slot :item="item" :i="i"></slot> | 		<slot :item="item" :i="i"></slot> | ||||||
| 		<div class="separator" :key="item.id + '_date'" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate()"> | 		<div class="separator" :key="item.id + '_date'" v-if="i != items.length - 1 && new Date(item.createdAt).getDate() != new Date(items[i + 1].createdAt).getDate() && !item._prInjectionId_ && !items[i + 1]._prInjectionId_"> | ||||||
| 			<p class="date"> | 			<p class="date"> | ||||||
| 				<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span> | 				<span><fa class="icon" :icon="faAngleUp"/>{{ getDateText(item.createdAt) }}</span> | ||||||
| 				<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span> | 				<span>{{ getDateText(items[i + 1].createdAt) }}<fa class="icon" :icon="faAngleDown"/></span> | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ | ||||||
| 	<x-sub v-for="note in conversation" :key="note.id" :note="note"/> | 	<x-sub v-for="note in conversation" :key="note.id" :note="note"/> | ||||||
| 	<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> | 	<x-sub :note="appearNote.reply" class="reply-to" v-if="appearNote.reply"/> | ||||||
| 	<div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div> | 	<div class="pinned" v-if="pinned"><fa :icon="faThumbtack"/> {{ $t('pinnedNote') }}</div> | ||||||
|  | 	<div class="pinned" v-if="appearNote._prInjectionId_"><fa :icon="faBullhorn"/> {{ $t('promotion') }}</div> | ||||||
| 	<div class="renote" v-if="isRenote"> | 	<div class="renote" v-if="isRenote"> | ||||||
| 		<mk-avatar class="avatar" :user="note.user"/> | 		<mk-avatar class="avatar" :user="note.user"/> | ||||||
| 		<fa :icon="faRetweet"/> | 		<fa :icon="faRetweet"/> | ||||||
|  | @ -83,7 +84,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons'; | import { faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | import { faCopy, faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { parse } from '../../mfm/parse'; | import { parse } from '../../mfm/parse'; | ||||||
| import { sum, unique } from '../../prelude/array'; | import { sum, unique } from '../../prelude/array'; | ||||||
|  | @ -140,7 +141,7 @@ export default Vue.extend({ | ||||||
| 			replies: [], | 			replies: [], | ||||||
| 			showContent: false, | 			showContent: false, | ||||||
| 			hideThisNote: false, | 			hideThisNote: false, | ||||||
| 			faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan | 			faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -522,6 +523,15 @@ export default Vue.extend({ | ||||||
| 					text: this.$t('pin'), | 					text: this.$t('pin'), | ||||||
| 					action: () => this.togglePin(true) | 					action: () => this.togglePin(true) | ||||||
| 				} : undefined, | 				} : undefined, | ||||||
|  | 				...(this.$store.state.i.isModerator || this.$store.state.i.isAdmin ? [ | ||||||
|  | 					null, | ||||||
|  | 					{ | ||||||
|  | 						icon: faBullhorn, | ||||||
|  | 						text: this.$t('promote'), | ||||||
|  | 						action: this.promote | ||||||
|  | 					}] | ||||||
|  | 					: [] | ||||||
|  | 				), | ||||||
| 				...(this.appearNote.userId == this.$store.state.i.id ? [ | 				...(this.appearNote.userId == this.$store.state.i.id ? [ | ||||||
| 					null, | 					null, | ||||||
| 					{ | 					{ | ||||||
|  | @ -614,6 +624,30 @@ export default Vue.extend({ | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		async promote() { | ||||||
|  | 			const { canceled, result: days } = await this.$root.dialog({ | ||||||
|  | 				title: this.$t('numberOfDays'), | ||||||
|  | 				input: { type: 'number' } | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 			this.$root.api('admin/promo/create', { | ||||||
|  | 				noteId: this.appearNote.id, | ||||||
|  | 				expiresAt: Date.now() + (86400000 * days) | ||||||
|  | 			}).then(() => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'success', | ||||||
|  | 					iconOnly: true, autoClose: true | ||||||
|  | 				}); | ||||||
|  | 			}).catch(e => { | ||||||
|  | 				this.$root.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: e | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		focus() { | 		focus() { | ||||||
| 			this.$el.focus(); | 			this.$el.focus(); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> | 	<x-list ref="notes" class="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed"> | ||||||
| 		<x-note :note="note" :detail="detail" :key="note.id"/> | 		<x-note :note="note" :detail="detail" :key="note._prInjectionId_ || note.id"/> | ||||||
| 	</x-list> | 	</x-list> | ||||||
| 
 | 
 | ||||||
| 	<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);"> | 	<div class="more" v-if="more && !reversed" style="margin-top: var(--margin);"> | ||||||
|  |  | ||||||
|  | @ -55,6 +55,8 @@ import { Clip } from '../models/entities/clip'; | ||||||
| import { ClipNote } from '../models/entities/clip-note'; | import { ClipNote } from '../models/entities/clip-note'; | ||||||
| import { Antenna } from '../models/entities/antenna'; | import { Antenna } from '../models/entities/antenna'; | ||||||
| import { AntennaNote } from '../models/entities/antenna-note'; | import { AntennaNote } from '../models/entities/antenna-note'; | ||||||
|  | import { PromoNote } from '../models/entities/promo-note'; | ||||||
|  | import { PromoRead } from '../models/entities/promo-read'; | ||||||
| 
 | 
 | ||||||
| const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | ||||||
| 
 | 
 | ||||||
|  | @ -140,6 +142,8 @@ export const entities = [ | ||||||
| 	ClipNote, | 	ClipNote, | ||||||
| 	Antenna, | 	Antenna, | ||||||
| 	AntennaNote, | 	AntennaNote, | ||||||
|  | 	PromoNote, | ||||||
|  | 	PromoRead, | ||||||
| 	ReversiGame, | 	ReversiGame, | ||||||
| 	ReversiMatching, | 	ReversiMatching, | ||||||
| 	...charts as any | 	...charts as any | ||||||
|  |  | ||||||
							
								
								
									
										28
									
								
								src/models/entities/promo-note.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/models/entities/promo-note.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; | ||||||
|  | import { Note } from './note'; | ||||||
|  | import { User } from './user'; | ||||||
|  | import { id } from '../id'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | export class PromoNote { | ||||||
|  | 	@PrimaryColumn(id()) | ||||||
|  | 	public noteId: Note['id']; | ||||||
|  | 
 | ||||||
|  | 	@OneToOne(type => Note, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public note: Note | null; | ||||||
|  | 
 | ||||||
|  | 	@Column('timestamp with time zone') | ||||||
|  | 	public expiresAt: Date; | ||||||
|  | 
 | ||||||
|  | 	//#region Denormalized fields
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column({ | ||||||
|  | 		...id(), | ||||||
|  | 		comment: '[Denormalized]' | ||||||
|  | 	}) | ||||||
|  | 	public userId: User['id']; | ||||||
|  | 	//#endregion
 | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								src/models/entities/promo-read.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/models/entities/promo-read.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; | ||||||
|  | import { Note } from './note'; | ||||||
|  | import { User } from './user'; | ||||||
|  | import { id } from '../id'; | ||||||
|  | 
 | ||||||
|  | @Entity() | ||||||
|  | @Index(['userId', 'noteId'], { unique: true }) | ||||||
|  | export class PromoRead { | ||||||
|  | 	@PrimaryColumn(id()) | ||||||
|  | 	public id: string; | ||||||
|  | 
 | ||||||
|  | 	@Column('timestamp with time zone', { | ||||||
|  | 		comment: 'The created date of the PromoRead.' | ||||||
|  | 	}) | ||||||
|  | 	public createdAt: Date; | ||||||
|  | 
 | ||||||
|  | 	@Index() | ||||||
|  | 	@Column(id()) | ||||||
|  | 	public userId: User['id']; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => User, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public user: User | null; | ||||||
|  | 
 | ||||||
|  | 	@Column(id()) | ||||||
|  | 	public noteId: Note['id']; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => Note, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public note: Note | null; | ||||||
|  | } | ||||||
|  | @ -50,6 +50,8 @@ import { ClipRepository } from './repositories/clip'; | ||||||
| import { ClipNote } from './entities/clip-note'; | import { ClipNote } from './entities/clip-note'; | ||||||
| import { AntennaRepository } from './repositories/antenna'; | import { AntennaRepository } from './repositories/antenna'; | ||||||
| import { AntennaNote } from './entities/antenna-note'; | import { AntennaNote } from './entities/antenna-note'; | ||||||
|  | import { PromoNote } from './entities/promo-note'; | ||||||
|  | import { PromoRead } from './entities/promo-read'; | ||||||
| 
 | 
 | ||||||
| export const Announcements = getRepository(Announcement); | export const Announcements = getRepository(Announcement); | ||||||
| export const AnnouncementReads = getRepository(AnnouncementRead); | export const AnnouncementReads = getRepository(AnnouncementRead); | ||||||
|  | @ -102,3 +104,5 @@ export const Clips = getCustomRepository(ClipRepository); | ||||||
| export const ClipNotes = getRepository(ClipNote); | export const ClipNotes = getRepository(ClipNote); | ||||||
| export const Antennas = getCustomRepository(AntennaRepository); | export const Antennas = getCustomRepository(AntennaRepository); | ||||||
| export const AntennaNotes = getRepository(AntennaNote); | export const AntennaNotes = getRepository(AntennaNote); | ||||||
|  | export const PromoNotes = getRepository(PromoNote); | ||||||
|  | export const PromoReads = getRepository(PromoRead); | ||||||
|  |  | ||||||
|  | @ -196,6 +196,7 @@ export class NoteRepository extends Repository<Note> { | ||||||
| 			renoteId: note.renoteId, | 			renoteId: note.renoteId, | ||||||
| 			mentions: note.mentions.length > 0 ? note.mentions : undefined, | 			mentions: note.mentions.length > 0 ? note.mentions : undefined, | ||||||
| 			uri: note.uri || undefined, | 			uri: note.uri || undefined, | ||||||
|  | 			_prInjectionId_: (note as any)._prInjectionId_ || undefined, | ||||||
| 
 | 
 | ||||||
| 			...(opts.detail ? { | 			...(opts.detail ? { | ||||||
| 				reply: note.replyId ? this.pack(note.replyId, meId, { | 				reply: note.replyId ? this.pack(note.replyId, meId, { | ||||||
|  |  | ||||||
							
								
								
									
										36
									
								
								src/server/api/common/inject-promo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/server/api/common/inject-promo.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | import rndstr from 'rndstr'; | ||||||
|  | import { Note } from '../../../models/entities/note'; | ||||||
|  | import { User } from '../../../models/entities/user'; | ||||||
|  | import { PromoReads, PromoNotes, Notes, Users } from '../../../models'; | ||||||
|  | import { ensure } from '../../../prelude/ensure'; | ||||||
|  | 
 | ||||||
|  | export async function injectPromo(user: User, timeline: Note[]) { | ||||||
|  | 	if (timeline.length < 5) return; | ||||||
|  | 
 | ||||||
|  | 	// TODO: readやexpireフィルタはクエリ側でやる
 | ||||||
|  | 
 | ||||||
|  | 	const reads = await PromoReads.find({ | ||||||
|  | 		userId: user.id | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	let promos = await PromoNotes.find(); | ||||||
|  | 
 | ||||||
|  | 	promos = promos.filter(n => n.expiresAt.getTime() > Date.now()); | ||||||
|  | 	promos = promos.filter(n => !reads.map(r => r.noteId).includes(n.noteId)); | ||||||
|  | 
 | ||||||
|  | 	if (promos.length === 0) return; | ||||||
|  | 
 | ||||||
|  | 	const promo = promos[Math.floor(Math.random() * promos.length)]; | ||||||
|  | 
 | ||||||
|  | 	// Pick random promo
 | ||||||
|  | 	const note = await Notes.findOne(promo.noteId).then(ensure); | ||||||
|  | 
 | ||||||
|  | 	// Join
 | ||||||
|  | 	note.user = await Users.findOne(note.userId).then(ensure); | ||||||
|  | 
 | ||||||
|  | 	(note as any)._prInjectionId_ = rndstr('a-z0-9', 8); | ||||||
|  | 
 | ||||||
|  | 	// Inject promo
 | ||||||
|  | 	timeline.splice(3, 0, note); | ||||||
|  | 	timeline.pop(); | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								src/server/api/endpoints/admin/promo/create.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/server/api/endpoints/admin/promo/create.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,58 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import { ID } from '../../../../../misc/cafy-id'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import { ApiError } from '../../../error'; | ||||||
|  | import { getNote } from '../../../common/getters'; | ||||||
|  | import { PromoNotes } from '../../../../../models'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	requireCredential: true as const, | ||||||
|  | 	requireModerator: true, | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		noteId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		expiresAt: { | ||||||
|  | 			validator: $.num.int() | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 		noSuchNote: { | ||||||
|  | 			message: 'No such note.', | ||||||
|  | 			code: 'NO_SUCH_NOTE', | ||||||
|  | 			id: 'ee449fbe-af2a-453b-9cae-cf2fe7c895fc' | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		alreadyPromoted: { | ||||||
|  | 			message: 'The note has already promoted.', | ||||||
|  | 			code: 'ALREADY_PROMOTED', | ||||||
|  | 			id: 'ae427aa2-7a41-484f-a18c-2c1104051604' | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user) => { | ||||||
|  | 	// Get favoritee
 | ||||||
|  | 	const note = await getNote(ps.noteId).catch(e => { | ||||||
|  | 		if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); | ||||||
|  | 		throw e; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	// if already favorited
 | ||||||
|  | 	const exist = await PromoNotes.findOne(note.id); | ||||||
|  | 
 | ||||||
|  | 	if (exist != null) { | ||||||
|  | 		throw new ApiError(meta.errors.alreadyPromoted); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Create favorite
 | ||||||
|  | 	await PromoNotes.save({ | ||||||
|  | 		noteId: note.id, | ||||||
|  | 		createdAt: new Date(), | ||||||
|  | 		expiresAt: new Date(ps.expiresAt), | ||||||
|  | 		userId: note.userId, | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | @ -7,8 +7,8 @@ import { makePaginationQuery } from '../../common/make-pagination-query'; | ||||||
| import { Notes } from '../../../../models'; | import { Notes } from '../../../../models'; | ||||||
| import { generateMuteQuery } from '../../common/generate-mute-query'; | import { generateMuteQuery } from '../../common/generate-mute-query'; | ||||||
| import { activeUsersChart } from '../../../../services/chart'; | import { activeUsersChart } from '../../../../services/chart'; | ||||||
| import { Brackets } from 'typeorm'; |  | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
|  | import { injectPromo } from '../../common/inject-promo'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
|  | @ -90,6 +90,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 
 | 
 | ||||||
| 	const timeline = await query.take(ps.limit!).getMany(); | 	const timeline = await query.take(ps.limit!).getMany(); | ||||||
| 
 | 
 | ||||||
|  | 	await injectPromo(user, timeline); | ||||||
|  | 
 | ||||||
| 	process.nextTick(() => { | 	process.nextTick(() => { | ||||||
| 		if (user) { | 		if (user) { | ||||||
| 			activeUsersChart.update(user); | 			activeUsersChart.update(user); | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | ||||||
| import { generateMuteQuery } from '../../common/generate-mute-query'; | import { generateMuteQuery } from '../../common/generate-mute-query'; | ||||||
| import { activeUsersChart } from '../../../../services/chart'; | import { activeUsersChart } from '../../../../services/chart'; | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
|  | import { injectPromo } from '../../common/inject-promo'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
|  | @ -169,6 +170,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 
 | 
 | ||||||
| 	const timeline = await query.take(ps.limit!).getMany(); | 	const timeline = await query.take(ps.limit!).getMany(); | ||||||
| 
 | 
 | ||||||
|  | 	await injectPromo(user, timeline); | ||||||
|  | 
 | ||||||
| 	process.nextTick(() => { | 	process.nextTick(() => { | ||||||
| 		if (user) { | 		if (user) { | ||||||
| 			activeUsersChart.update(user); | 			activeUsersChart.update(user); | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query' | ||||||
| import { activeUsersChart } from '../../../../services/chart'; | import { activeUsersChart } from '../../../../services/chart'; | ||||||
| import { Brackets } from 'typeorm'; | import { Brackets } from 'typeorm'; | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
|  | import { injectPromo } from '../../common/inject-promo'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
|  | @ -122,6 +123,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 
 | 
 | ||||||
| 	const timeline = await query.take(ps.limit!).getMany(); | 	const timeline = await query.take(ps.limit!).getMany(); | ||||||
| 
 | 
 | ||||||
|  | 	await injectPromo(user, timeline); | ||||||
|  | 
 | ||||||
| 	process.nextTick(() => { | 	process.nextTick(() => { | ||||||
| 		if (user) { | 		if (user) { | ||||||
| 			activeUsersChart.update(user); | 			activeUsersChart.update(user); | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import { generateMuteQuery } from '../../common/generate-mute-query'; | ||||||
| import { activeUsersChart } from '../../../../services/chart'; | import { activeUsersChart } from '../../../../services/chart'; | ||||||
| import { Brackets } from 'typeorm'; | import { Brackets } from 'typeorm'; | ||||||
| import { generateRepliesQuery } from '../../common/generate-replies-query'; | import { generateRepliesQuery } from '../../common/generate-replies-query'; | ||||||
|  | import { injectPromo } from '../../common/inject-promo'; | ||||||
| 
 | 
 | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
|  | @ -155,6 +156,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 
 | 
 | ||||||
| 	const timeline = await query.take(ps.limit!).getMany(); | 	const timeline = await query.take(ps.limit!).getMany(); | ||||||
| 
 | 
 | ||||||
|  | 	await injectPromo(user, timeline); | ||||||
|  | 
 | ||||||
| 	process.nextTick(() => { | 	process.nextTick(() => { | ||||||
| 		if (user) { | 		if (user) { | ||||||
| 			activeUsersChart.update(user); | 			activeUsersChart.update(user); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue