162 lines
5.0 KiB
TypeScript
162 lines
5.0 KiB
TypeScript
import { VisibilityType } from 'mastodon'
|
|
import Rest from './Rest'
|
|
import { Action, ActionConfig, ActionConfigData, RootConfig, VISIBILITIES } from './types/config'
|
|
import Logger from './utils/Logger'
|
|
import matcher from './utils/matcher'
|
|
import Selector from './utils/Selector'
|
|
|
|
export default class App {
|
|
|
|
constructor(private rest: Rest) { }
|
|
|
|
async run(rootId: string) {
|
|
this.load(rootId, this.process.bind(this))
|
|
}
|
|
|
|
async load(rootId: string, process: (me: string, action: Action) => Promise<any>) {
|
|
const { id: me } = await this.login()
|
|
|
|
const config = await this.loadRoot(rootId, me)
|
|
Logger.debug('Root config', config)
|
|
if (!config.botodon) {
|
|
Logger.error('Root status requires #botodon tag')
|
|
return
|
|
}
|
|
|
|
for (const action of await this.loadActions(rootId, config.deep, config.shared ? undefined : me)) {
|
|
const promise = process(me, action)
|
|
if (!config.async) {
|
|
await promise
|
|
}
|
|
}
|
|
}
|
|
|
|
async login() {
|
|
const me = await this.rest.getMe()
|
|
Logger.debug(`Logged as ${me.acct}`)
|
|
if (!me.bot) {
|
|
Logger.warn('Please set account as bot !')
|
|
}
|
|
return me
|
|
}
|
|
|
|
async loadRoot(id: string, me: string) {
|
|
const status = await this.rest.getStatus(id)
|
|
if (status.account.id !== me) {
|
|
Logger.warn('Root status isn\'t yours')
|
|
}
|
|
|
|
return matcher<RootConfig>({
|
|
async: false,
|
|
botodon: false,
|
|
deep: false,
|
|
shared: false
|
|
}, status.tags.map(t => t.name).reverse())
|
|
}
|
|
|
|
async loadActions(id: string, deep: boolean, account?: string) {
|
|
const lines: Action[] = (await this.rest.getReplies(id, deep, account))
|
|
.map(s => ({ id: s.id, tags: s.tags.map(t => t.name) }))
|
|
|
|
Logger.debug(`Found ${lines.length} action(s)`)
|
|
if (!lines.length) {
|
|
Logger.error('Root status is empty !')
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
async process(me: string, action: Action) {
|
|
const config = matcher<ActionConfig>({
|
|
botodon: false,
|
|
data: {
|
|
from: [],
|
|
deep: false,
|
|
shared: false,
|
|
tagged: [],
|
|
favourited: false,
|
|
favourites: 0,
|
|
last: -1,
|
|
weighted: false,
|
|
same: false
|
|
},
|
|
global: false,
|
|
followers: false,
|
|
followers_of: [],
|
|
replies: {
|
|
to: [],
|
|
deep: false,
|
|
visibility: false
|
|
},
|
|
favourites: [],
|
|
visibility: 'unlisted'
|
|
}, action.tags.reverse())
|
|
|
|
Logger.debug(`Action ${action.id} config`, config)
|
|
if (!config.botodon) {
|
|
Logger.error(`Action status ${action.id} requires #botodon tag`)
|
|
return
|
|
}
|
|
if (!VISIBILITIES.includes(config.visibility)) {
|
|
Logger.error(`Action status ${action.id}: invalid visibility ${config.visibility}`)
|
|
return
|
|
}
|
|
|
|
// Data
|
|
const datas = (await Promise.all(config.data.from.map(id => this.loadData(id, config.data, me))))
|
|
.reduce((a, b) => a.concat(b))
|
|
if (!datas.length) {
|
|
Logger.error(`Action ${action.id}: Any content`)
|
|
return
|
|
}
|
|
const selector = new Selector(datas, config.data.same, s => config.data.weighted ? s.favourites_count : 1)
|
|
|
|
// Targets
|
|
// TODO: progressive send (limit memory usage)
|
|
const targets: Array<{acct?: string, visibility: VisibilityType}> = []
|
|
if (config.global) {
|
|
targets.push({ visibility: config.visibility })
|
|
}
|
|
if (config.followers) {
|
|
config.followers_of.push(me)
|
|
}
|
|
for await (const followers of config.followers_of.map(id => this.rest.getFollowers(id))) {
|
|
targets.push(...followers.map(({ acct }) => ({ acct, visibility: config.visibility })))
|
|
}
|
|
for await (const replies of config.replies.to.map(id => this.rest.getReplies(id, config.replies.deep))) {
|
|
targets.push(...replies.map(({ account: { acct }, visibility }) => ({
|
|
acct, visibility: config.replies.visibility ? visibility : config.visibility
|
|
})))
|
|
}
|
|
for await (const fav of config.favourites.map(id => this.rest.getFavouritedBy(id))) {
|
|
targets.push(...fav.map(({ acct }) => ({ acct, visibility: config.visibility })))
|
|
}
|
|
Logger.debug(`Action ${action.id}: ${targets.length} target(s)`)
|
|
if (!targets.length) {
|
|
Logger.warn(`Action ${action.id}: Any target`)
|
|
}
|
|
|
|
for (const target of targets) {
|
|
const next = selector.next()
|
|
await this.rest.postStatus({
|
|
media_ids: next.media_attachments.map(m => m.id),
|
|
sensitive: next.sensitive,
|
|
spoiler_text: next.spoiler_text,
|
|
status: `${target.acct ? `@${target.acct} ` : ''}${next.content.replace(/<[^>]*>?/gm, '')}`,
|
|
visibility: target.visibility
|
|
})
|
|
}
|
|
Logger.debug(`Action ${action.id}: done`)
|
|
}
|
|
|
|
async loadData(id: string, config: ActionConfigData, me: string) {
|
|
const replies = await this.rest.getReplies(id, config.deep, config.shared ? undefined : me)
|
|
|
|
return replies
|
|
.filter(s => !config.favourited || s.favourited)
|
|
.filter(s => s.favourites_count >= config.favourites)
|
|
.filter(s => config.tagged.every(tag => s.tags.map(t => t.name).includes(tag)))
|
|
.slice(0, config.last === -1 ? undefined : config.last)
|
|
}
|
|
|
|
} |