diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-05-04 21:15:57 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-05-04 21:15:57 +0900 |
| commit | 18e1efc7ecd3f5a6d774c16f17526d12ae46b2f5 (patch) | |
| tree | 8f2cb50644bb3679eafd29fb9e7448ed5069321c /src | |
| parent | メールアドレスの設定を促すように (diff) | |
| download | sharkey-18e1efc7ecd3f5a6d774c16f17526d12ae46b2f5.tar.gz sharkey-18e1efc7ecd3f5a6d774c16f17526d12ae46b2f5.tar.bz2 sharkey-18e1efc7ecd3f5a6d774c16f17526d12ae46b2f5.zip | |
Ad (#7495)
* wip
* Update ad.vue
* Update default.widgets.vue
* wip
* Create 1620019354680-ad.ts
* wip
* Update ads.vue
* wip
* Update ad.vue
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/components/date-separated-list.vue | 22 | ||||
| -rw-r--r-- | src/client/components/global/ad.vue | 142 | ||||
| -rw-r--r-- | src/client/components/index.ts | 4 | ||||
| -rw-r--r-- | src/client/components/notes.vue | 2 | ||||
| -rw-r--r-- | src/client/pages/gallery/post.vue | 1 | ||||
| -rw-r--r-- | src/client/pages/instance/ads.vue | 125 | ||||
| -rw-r--r-- | src/client/pages/instance/index.vue | 2 | ||||
| -rw-r--r-- | src/client/pages/page.vue | 1 | ||||
| -rw-r--r-- | src/client/scripts/paging.ts | 8 | ||||
| -rw-r--r-- | src/client/style.scss | 2 | ||||
| -rw-r--r-- | src/client/ui/chat/date-separated-list.vue | 6 | ||||
| -rw-r--r-- | src/client/ui/default.widgets.vue | 1 | ||||
| -rw-r--r-- | src/db/postgre.ts | 2 | ||||
| -rw-r--r-- | src/models/entities/ad.ts | 53 | ||||
| -rw-r--r-- | src/models/index.ts | 2 | ||||
| -rw-r--r-- | src/models/repositories/note.ts | 11 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/ad/create.ts | 45 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/ad/delete.ts | 34 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/ad/list.ts | 36 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/ad/update.ts | 59 | ||||
| -rw-r--r-- | src/server/api/endpoints/meta.ts | 39 |
21 files changed, 571 insertions, 26 deletions
diff --git a/src/client/components/date-separated-list.vue b/src/client/components/date-separated-list.vue index 2a861adb09..d458a0eeb8 100644 --- a/src/client/components/date-separated-list.vue +++ b/src/client/components/date-separated-list.vue @@ -1,5 +1,6 @@ <script lang="ts"> import { defineComponent, h, TransitionGroup } from 'vue'; +import MkAd from '@client/components/global/ad.vue'; export default defineComponent({ props: { @@ -22,6 +23,11 @@ export default defineComponent({ required: false, default: false }, + ad: { + type: Boolean, + required: false, + default: false + }, }, methods: { @@ -58,11 +64,7 @@ export default defineComponent({ if ( i != this.items.length - 1 && - new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() && - !item._prId_ && - !this.items[i + 1]._prId_ && - !item._featuredId_ && - !this.items[i + 1]._featuredId_ + new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() ) { const separator = h('div', { class: 'separator', @@ -86,7 +88,15 @@ export default defineComponent({ return [el, separator]; } else { - return el; + if (this.ad && item._shouldInsertAd_) { + return [h(MkAd, { + class: 'ad', + key: item.id + ':ad', + prefer: 'horizontal', + }), el]; + } else { + return el; + } } })); }, diff --git a/src/client/components/global/ad.vue b/src/client/components/global/ad.vue new file mode 100644 index 0000000000..00592e4ca2 --- /dev/null +++ b/src/client/components/global/ad.vue @@ -0,0 +1,142 @@ +<template> +<div class="qiivuoyo" v-if="ad"> + <div class="main" :class="ad.place" v-if="!showMenu"> + <a :href="ad.url" target="_blank"> + <img :src="ad.imageUrl"> + <button class="_button menu" @click.prevent.stop="toggleMenu"><span class="fas fa-info-circle"></span></button> + </a> + </div> + <div class="menu" v-else> + <div class="body"> + <div>Ads by {{ host }}</div> + <!--<MkButton>{{ $ts.stopThisAd }}</MkButton>--> + <button class="_textButton" @click="toggleMenu">{{ $ts.close }}</button> + </div> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent, ref } from 'vue'; +import { instance } from '@client/instance'; +import { host } from '@client/config'; +import MkButton from '@client/components/ui/button.vue'; + +export default defineComponent({ + components: { + MkButton + }, + + props: { + prefer: { + type: String, + required: true + }, + ad: { + type: Object, + required: false + }, + }, + + setup(props) { + const showMenu = ref(false); + const toggleMenu = () => { + showMenu.value = !showMenu.value; + }; + + let ad = null; + + if (props.ad) { + ad = props.ad; + } else { + let ads = instance.ads.filter(ad => ad.place === props.prefer); + + if (ads.length === 0) { + ads = instance.ads.filter(ad => ad.place === 'square'); + } + + const high = ads.filter(ad => ad.priority === 'high'); + const middle = ads.filter(ad => ad.priority === 'middle'); + const low = ads.filter(ad => ad.priority === 'low'); + + if (high.length > 0) { + ad = high[Math.floor(Math.random() * high.length)]; + } else if (middle.length > 0) { + ad = middle[Math.floor(Math.random() * middle.length)]; + } else if (low.length > 0) { + ad = low[Math.floor(Math.random() * low.length)]; + } + } + + return { + ad, + showMenu, + toggleMenu, + host, + }; + } +}); +</script> + +<style lang="scss" scoped> +.qiivuoyo { + background-size: auto auto; + background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px ); + + > .main { + > a { + display: block; + position: relative; + margin: 0 auto; + + > img { + display: block; + width: 100%; + height: 100%; + object-fit: contain; + } + + > .menu { + position: absolute; + top: 0; + right: 0; + background: var(--panel); + } + } + + &.square { + > a { + max-width: min(300px, 100%); + max-height: min(300px, 100%); + } + } + + &.horizontal { + padding: 8px; + + > a { + max-width: min(600px, 100%); + max-height: min(100px, 100%); + } + } + + &.vertical { + > a { + max-width: min(100px, 100%); + } + } + } + + > .menu { + padding: 8px; + text-align: center; + + > .body { + padding: 8px; + margin: 0 auto; + max-width: 400px; + border: solid 1px var(--divider); + } + } +} +</style> diff --git a/src/client/components/index.ts b/src/client/components/index.ts index 0630ed3d8c..8b914c5eec 100644 --- a/src/client/components/index.ts +++ b/src/client/components/index.ts @@ -12,8 +12,10 @@ import url from './global/url.vue'; import i18n from './global/i18n'; import loading from './global/loading.vue'; import error from './global/error.vue'; +import ad from './global/ad.vue'; export default function(app: App) { + app.component('I18n', i18n); app.component('Mfm', mfm); app.component('MkA', a); app.component('MkAcct', acct); @@ -25,5 +27,5 @@ export default function(app: App) { app.component('MkUrl', url); app.component('MkLoading', loading); app.component('MkError', error); - app.component('I18n', i18n); + app.component('MkAd', ad); } diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue index 675748d540..e90102921a 100644 --- a/src/client/components/notes.vue +++ b/src/client/components/notes.vue @@ -17,7 +17,7 @@ </MkButton> </div> - <XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap"> + <XList ref="notes" :items="notes" v-slot="{ item: note }" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true"> <XNote :note="note" class="_block" @update:note="updated(note, $event)" :key="note._featuredId_ || note._prId_ || note.id"/> </XList> diff --git a/src/client/pages/gallery/post.vue b/src/client/pages/gallery/post.vue index 703506a78d..50f81376ec 100644 --- a/src/client/pages/gallery/post.vue +++ b/src/client/pages/gallery/post.vue @@ -33,6 +33,7 @@ <MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/> </div> </div> + <MkAd prefer="horizontal"/> <MkContainer :max-height="300" :foldable="true" class="other"> <template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template> <MkPagination :pagination="otherPostsPagination" #default="{items}"> diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/instance/ads.vue new file mode 100644 index 0000000000..4297e56c37 --- /dev/null +++ b/src/client/pages/instance/ads.vue @@ -0,0 +1,125 @@ +<template> +<div class="uqshojas"> + <MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton> + <section class="_card _gap ads" v-for="ad in ads"> + <div class="_content ad"> + <MkAd v-if="ad.url" :ad="ad"/> + <MkInput v-model:value="ad.url" type="url"> + <span>URL</span> + </MkInput> + <MkInput v-model:value="ad.imageUrl"> + <span>{{ $ts.imageUrl }}</span> + </MkInput> + <div style="margin: 32px 0;"> + <MkRadio v-model="ad.place" value="square">square</MkRadio> + <MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio> + </div> + <div style="margin: 32px 0;"> + {{ $ts.priority }} + <MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio> + <MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio> + <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio> + </div> + <MkInput v-model:value="ad.expiresAt" type="date"> + <span>{{ $ts.expiration }}</span> + </MkInput> + <MkTextarea v-model:value="ad.memo"> + <span>{{ $ts.memo }}</span> + </MkTextarea> + <div class="buttons"> + <MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> + <MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton> + </div> + </div> + </section> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import MkButton from '@client/components/ui/button.vue'; +import MkInput from '@client/components/ui/input.vue'; +import MkTextarea from '@client/components/ui/textarea.vue'; +import MkRadio from '@client/components/ui/radio.vue'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; + +export default defineComponent({ + components: { + MkButton, + MkInput, + MkTextarea, + MkRadio, + }, + + emits: ['info'], + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts.ads, + icon: 'fas fa-audio-description' + }, + ads: [], + } + }, + + created() { + os.api('admin/ad/list').then(ads => { + this.ads = ads; + }); + }, + + mounted() { + this.$emit('info', this[symbols.PAGE_INFO]); + }, + + methods: { + add() { + this.ads.unshift({ + id: null, + memo: '', + place: 'square', + priority: 'middle', + url: '', + imageUrl: null, + expiresAt: null, + }); + }, + + remove(ad) { + os.dialog({ + type: 'warning', + text: this.$t('removeAreYouSure', { x: ad.url }), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + this.ads = this.ads.filter(x => x != ad); + os.apiWithDialog('admin/ad/delete', { + id: ad.id + }); + }); + }, + + save(ad) { + if (ad.id == null) { + os.apiWithDialog('admin/ad/create', { + ...ad, + expiresAt: new Date(ad.expiresAt).getTime() + }); + } else { + os.apiWithDialog('admin/ad/update', { + ...ad, + expiresAt: new Date(ad.expiresAt).getTime() + }); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.uqshojas { + margin: var(--margin); +} +</style> diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue index 5972a02de0..974c4345bb 100644 --- a/src/client/pages/instance/index.vue +++ b/src/client/pages/instance/index.vue @@ -23,6 +23,7 @@ <FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink> <FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink> <FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink> + <FormLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</FormLink> <FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink> </FormGroup> <FormGroup> @@ -102,6 +103,7 @@ export default defineComponent({ case 'queue': return defineAsyncComponent(() => import('./queue.vue')); case 'files': return defineAsyncComponent(() => import('./files.vue')); case 'announcements': return defineAsyncComponent(() => import('./announcements.vue')); + case 'ads': return defineAsyncComponent(() => import('./ads.vue')); case 'database': return defineAsyncComponent(() => import('./database.vue')); case 'abuses': return defineAsyncComponent(() => import('./abuses.vue')); case 'settings': return defineAsyncComponent(() => import('./settings.vue')); diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue index 6ee3ee8d26..4e237c2186 100644 --- a/src/client/pages/page.vue +++ b/src/client/pages/page.vue @@ -45,6 +45,7 @@ <div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div> <div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div> </div> + <MkAd prefer="horizontal"/> <MkContainer :max-height="300" :foldable="true" class="other"> <template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template> <MkPagination :pagination="otherPostsPagination" #default="{items}"> diff --git a/src/client/scripts/paging.ts b/src/client/scripts/paging.ts index 2e49f1a64c..bcb0d7f2b0 100644 --- a/src/client/scripts/paging.ts +++ b/src/client/scripts/paging.ts @@ -91,8 +91,10 @@ export default (opts) => ({ ...params, limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1, }).then(items => { - for (const item of items) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; markRaw(item); + if (i === 3) item._shouldInsertAd_ = true; } if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) { items.pop(); @@ -128,8 +130,10 @@ export default (opts) => ({ untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id, }), }).then(items => { - for (const item of items) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; markRaw(item); + if (i === 10) item._shouldInsertAd_ = true; } if (items.length > SECOND_FETCH_LIMIT) { items.pop(); diff --git a/src/client/style.scss b/src/client/style.scss index 523ab13034..39bf6ef2d5 100644 --- a/src/client/style.scss +++ b/src/client/style.scss @@ -11,6 +11,8 @@ @media (max-width: 500px) { --margin: var(--marginHalf); } + + //--ad: rgb(255 169 0 / 10%); } ::selection { diff --git a/src/client/ui/chat/date-separated-list.vue b/src/client/ui/chat/date-separated-list.vue index b073a38eb1..bc7fc91d38 100644 --- a/src/client/ui/chat/date-separated-list.vue +++ b/src/client/ui/chat/date-separated-list.vue @@ -42,11 +42,7 @@ export default defineComponent({ if ( i != this.items.length - 1 && - new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() && - !item._prId_ && - !this.items[i + 1]._prId_ && - !item._featuredId_ && - !this.items[i + 1]._featuredId_ + new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() ) { const separator = h('div', { class: 'separator', diff --git a/src/client/ui/default.widgets.vue b/src/client/ui/default.widgets.vue index cabd83937e..0dd073409b 100644 --- a/src/client/ui/default.widgets.vue +++ b/src/client/ui/default.widgets.vue @@ -1,6 +1,7 @@ <template> <div class="efzpzdvf"> <XWidgets class="widgets" :edit="editMode" :widgets="$store.reactiveState.widgets.value" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/> + <MkAd prefer="square"/> <button v-if="editMode" @click="editMode = false" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-check"></i> {{ $ts.editWidgetsExit }}</button> <button v-else @click="editMode = true" class="_textButton edit" style="font-size: 0.9em;"><i class="fas fa-pencil-alt"></i> {{ $ts.editWidgets }}</button> diff --git a/src/db/postgre.ts b/src/db/postgre.ts index e2a779a52d..3ad81203f2 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -70,6 +70,7 @@ import { Channel } from '../models/entities/channel'; import { ChannelFollowing } from '../models/entities/channel-following'; import { ChannelNotePining } from '../models/entities/channel-note-pining'; import { RegistryItem } from '../models/entities/registry-item'; +import { Ad } from '../models/entities/ad'; import { PasswordResetRequest } from '@/models/entities/password-reset-request'; const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); @@ -170,6 +171,7 @@ export const entities = [ ChannelFollowing, ChannelNotePining, RegistryItem, + Ad, PasswordResetRequest, ...charts as any ]; diff --git a/src/models/entities/ad.ts b/src/models/entities/ad.ts new file mode 100644 index 0000000000..3279de29ea --- /dev/null +++ b/src/models/entities/ad.ts @@ -0,0 +1,53 @@ +import { Entity, Index, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Ad { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Ad.' + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + comment: 'The expired date of the Ad.' + }) + public expiresAt: Date; + + @Column('varchar', { + length: 32, nullable: false + }) + public place: string; + + @Column('varchar', { + length: 32, nullable: false + }) + public priority: string; + + @Column('varchar', { + length: 1024, nullable: false + }) + public url: string; + + @Column('varchar', { + length: 1024, nullable: false + }) + public imageUrl: string; + + @Column('varchar', { + length: 8192, nullable: false + }) + public memo: string; + + constructor(data: Partial<Ad>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 6ce453ef33..9f8bd104e9 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -60,6 +60,7 @@ import { MutedNote } from './entities/muted-note'; import { ChannelFollowing } from './entities/channel-following'; import { ChannelNotePining } from './entities/channel-note-pining'; import { RegistryItem } from './entities/registry-item'; +import { Ad } from './entities/ad'; import { PasswordResetRequest } from './entities/password-reset-request'; export const Announcements = getRepository(Announcement); @@ -123,4 +124,5 @@ export const Channels = getCustomRepository(ChannelRepository); export const ChannelFollowings = getRepository(ChannelFollowing); export const ChannelNotePinings = getRepository(ChannelNotePining); export const RegistryItems = getRepository(RegistryItem); +export const Ads = getRepository(Ad); export const PasswordResetRequests = getRepository(PasswordResetRequest); diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index cdf4841918..7b1df73024 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -200,8 +200,6 @@ export class NoteRepository extends Repository<Note> { mentions: note.mentions.length > 0 ? note.mentions : undefined, uri: note.uri || undefined, url: note.url || undefined, - _featuredId_: (note as any)._featuredId_ || undefined, - _prId_: (note as any)._prId_ || undefined, ...(opts.detail ? { reply: note.replyId ? this.pack(note.reply || note.replyId, me, { @@ -448,14 +446,7 @@ export const packedNoteSchema = { optional: false as const, nullable: true as const, description: 'The human readable url of a note. it will be null when the note is local.', }, - _featuredId_: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, - _prId_: { - type: 'string' as const, - optional: false as const, nullable: true as const, - }, + myReaction: { type: 'object' as const, optional: true as const, nullable: true as const, diff --git a/src/server/api/endpoints/admin/ad/create.ts b/src/server/api/endpoints/admin/ad/create.ts new file mode 100644 index 0000000000..7777e95e6e --- /dev/null +++ b/src/server/api/endpoints/admin/ad/create.ts @@ -0,0 +1,45 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { Ads } from '../../../../../models'; +import { genId } from '@/misc/gen-id'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + url: { + validator: $.str.min(1) + }, + memo: { + validator: $.str + }, + place: { + validator: $.str + }, + priority: { + validator: $.str + }, + expiresAt: { + validator: $.num.int() + }, + imageUrl: { + validator: $.str.min(1) + } + }, +}; + +export default define(meta, async (ps) => { + await Ads.insert({ + id: genId(), + createdAt: new Date(), + expiresAt: new Date(ps.expiresAt), + url: ps.url, + imageUrl: ps.imageUrl, + priority: ps.priority, + place: ps.place, + memo: ps.memo, + }); +}); diff --git a/src/server/api/endpoints/admin/ad/delete.ts b/src/server/api/endpoints/admin/ad/delete.ts new file mode 100644 index 0000000000..6a5f92193e --- /dev/null +++ b/src/server/api/endpoints/admin/ad/delete.ts @@ -0,0 +1,34 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Ads } from '../../../../../models'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + id: { + validator: $.type(ID) + } + }, + + errors: { + noSuchAd: { + message: 'No such ad.', + code: 'NO_SUCH_AD', + id: 'ccac9863-3a03-416e-b899-8a64041118b1' + } + } +}; + +export default define(meta, async (ps, me) => { + const ad = await Ads.findOne(ps.id); + + if (ad == null) throw new ApiError(meta.errors.noSuchAd); + + await Ads.delete(ad.id); +}); diff --git a/src/server/api/endpoints/admin/ad/list.ts b/src/server/api/endpoints/admin/ad/list.ts new file mode 100644 index 0000000000..a323f2a9ed --- /dev/null +++ b/src/server/api/endpoints/admin/ad/list.ts @@ -0,0 +1,36 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { Ads } from '../../../../../models'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, + + sinceId: { + validator: $.optional.type(ID), + }, + + untilId: { + validator: $.optional.type(ID), + }, + }, +}; + +export default define(meta, async (ps) => { + const query = makePaginationQuery(Ads.createQueryBuilder('ad'), ps.sinceId, ps.untilId) + .andWhere('ad.expiresAt > :now', { now: new Date() }); + + const ads = await query.take(ps.limit!).getMany(); + + return ads; +}); diff --git a/src/server/api/endpoints/admin/ad/update.ts b/src/server/api/endpoints/admin/ad/update.ts new file mode 100644 index 0000000000..694af98394 --- /dev/null +++ b/src/server/api/endpoints/admin/ad/update.ts @@ -0,0 +1,59 @@ +import $ from 'cafy'; +import define from '../../../define'; +import { ID } from '@/misc/cafy-id'; +import { Ads } from '../../../../../models'; +import { ApiError } from '../../../error'; + +export const meta = { + tags: ['admin'], + + requireCredential: true as const, + requireModerator: true, + + params: { + id: { + validator: $.type(ID) + }, + memo: { + validator: $.str + }, + url: { + validator: $.str.min(1) + }, + imageUrl: { + validator: $.str.min(1) + }, + place: { + validator: $.str + }, + priority: { + validator: $.str + }, + expiresAt: { + validator: $.num.int() + }, + }, + + errors: { + noSuchAd: { + message: 'No such ad.', + code: 'NO_SUCH_AD', + id: 'b7aa1727-1354-47bc-a182-3a9c3973d300' + } + } +}; + +export default define(meta, async (ps, me) => { + const ad = await Ads.findOne(ps.id); + + if (ad == null) throw new ApiError(meta.errors.noSuchAd); + + await Ads.update(ad.id, { + url: ps.url, + place: ps.place, + priority: ps.priority, + memo: ps.memo, + imageUrl: ps.imageUrl, + expiresAt: new Date(ps.expiresAt), + }); +}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 3760c8b37b..5b7292ef16 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -2,8 +2,9 @@ import $ from 'cafy'; import config from '@/config'; import define from '../define'; import { fetchMeta } from '@/misc/fetch-meta'; -import { Emojis, Users } from '../../../models'; +import { Ads, Emojis, Users } from '../../../models'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/misc/hard-limits'; +import { MoreThan } from 'typeorm'; export const meta = { desc: { @@ -193,6 +194,30 @@ export const meta = { } } }, + ads: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + place: { + type: 'string' as const, + optional: false as const, nullable: false as const + }, + url: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'url' + }, + imageUrl: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'url' + }, + } + } + }, requireSetup: { type: 'boolean' as const, optional: false as const, nullable: false as const, @@ -443,6 +468,12 @@ export default define(meta, async (ps, me) => { } }); + const ads = await Ads.find({ + where: { + expiresAt: MoreThan(new Date()) + }, + }); + const response: any = { maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, @@ -477,6 +508,12 @@ export default define(meta, async (ps, me) => { logoImageUrl: instance.logoImageUrl, maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH), emojis: await Emojis.packMany(emojis), + ads: ads.map(ad => ({ + url: ad.url, + place: ad.place, + priority: ad.priority, + imageUrl: ad.imageUrl, + })), enableEmail: instance.enableEmail, enableTwitterIntegration: instance.enableTwitterIntegration, |