feat(platform): v0.8.0 announcement follow and cross-post
Mirror announcement posts to configured text channels via P2P gossip, per-user follow with announcement notifications, server settings APIs, and view fields for UI. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -25,6 +25,10 @@ const { PearcordForum } = require('pearcord-forum')
|
||||
const { PearcordStage, STAGE_ROLE, isStageChannel } = require('pearcord-stage')
|
||||
const { PearcordAutomod } = require('pearcord-automod')
|
||||
const { ScreenShareHub } = require('pearcord-screen-share')
|
||||
const {
|
||||
PearcordAnnouncements,
|
||||
formatCrosspostBody
|
||||
} = require('pearcord-announcements')
|
||||
const {
|
||||
createInstallToken,
|
||||
verifyInstallToken,
|
||||
@@ -79,6 +83,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.voice = null
|
||||
this.voiceMedia = null
|
||||
this.screenShare = null
|
||||
this.announcements = null
|
||||
this.slashCommands = null
|
||||
this.emojiRegistry = null
|
||||
this.botEvents = null
|
||||
@@ -216,6 +221,8 @@ class PearcordPlatform extends EventEmitter {
|
||||
await this.stage.ready()
|
||||
this.automod = new PearcordAutomod({ storagePath: this.storagePath })
|
||||
await this.automod.ready()
|
||||
this.announcements = new PearcordAnnouncements({ storagePath: this.storagePath })
|
||||
await this.announcements.ready()
|
||||
this._forumTagFilter = null
|
||||
this.guilds = await this.listGuilds()
|
||||
await this._joinDiscoveryMesh()
|
||||
@@ -768,6 +775,14 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.screenShare?.ingestGossip(p).catch(() => {})
|
||||
this.emit('screen-share', p)
|
||||
})
|
||||
guildInstance.on('announcement-crosspost', (p) => {
|
||||
this.announcements?.ingestCrosspostConfig(p).catch(() => {})
|
||||
this.emit('announcements', p)
|
||||
})
|
||||
guildInstance.on('announcement-follow', (p) => {
|
||||
this.announcements?.ingestFollow(p).catch(() => {})
|
||||
this.emit('announcements', p)
|
||||
})
|
||||
guildInstance.setAttachmentProvider(async (attachmentId) => {
|
||||
const row = await this.attachments?.get(attachmentId)
|
||||
if (!row) throw new Error('attachment not found')
|
||||
@@ -917,6 +932,21 @@ class PearcordPlatform extends EventEmitter {
|
||||
guildId: msg.guildId,
|
||||
id: msg.channelId
|
||||
})
|
||||
if (ch?.type === 'announcement' && this.announcements) {
|
||||
const following = await this.announcements.isFollowing(
|
||||
msg.guildId,
|
||||
msg.channelId,
|
||||
user.id
|
||||
)
|
||||
if (following) {
|
||||
await this._notifyAnnouncementFollow({
|
||||
message: { ...msg, content: plain },
|
||||
guildName: this.guild?.guild?.name,
|
||||
channelName: ch.name,
|
||||
authorName: await this._authorDisplayName(msg.authorId, members, usersById)
|
||||
})
|
||||
}
|
||||
}
|
||||
await this._maybeNotify({
|
||||
message: { ...msg, content: plain },
|
||||
mode: msg.guildId === DM_GUILD_ID ? 'dm' : 'guild',
|
||||
@@ -1939,6 +1969,10 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.automod = new PearcordAutomod({ storagePath: this.storagePath })
|
||||
await this.automod.ready()
|
||||
}
|
||||
if (!this.announcements) {
|
||||
this.announcements = new PearcordAnnouncements({ storagePath: this.storagePath })
|
||||
await this.announcements.ready()
|
||||
}
|
||||
const guildId = this.guild?.guild?.id
|
||||
if (guildId && this.automod) this.automod.setGuild(guildId)
|
||||
return { forum: this.forum, stage: this.stage, automod: this.automod }
|
||||
@@ -2211,11 +2245,149 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.guild.gossipMessage(msg)
|
||||
this._queueLinkEmbed(msg).catch(() => {})
|
||||
this._fanoutBotEvent(BOT_EVENTS.MESSAGE_CREATE, { message: msg })
|
||||
if (this.mode === 'guild') {
|
||||
const channels = await this.guild.listChannels()
|
||||
const srcCh = channels.find((c) => c.id === msg.channelId)
|
||||
if (srcCh && isAnnouncementChannel(srcCh)) {
|
||||
await this._crosspostAnnouncement(msg, srcCh)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.emit('message', msg)
|
||||
return msg
|
||||
}
|
||||
|
||||
async _insertGuildMessage ({ guildId, channelId, content, authorId }) {
|
||||
if (!this.messages || !guildId || !channelId) throw new Error('invalid message insert')
|
||||
const prevChannel = this.messages.channelId
|
||||
const prevGuild = this.messages.guildId
|
||||
this.messages.setChannel({ channelId, guildId })
|
||||
const row = await this.messages.send(content, { authorId: authorId || this.identity.user?.id })
|
||||
this.messages.setChannel({ channelId: prevChannel, guildId: prevGuild })
|
||||
if (this.guild) {
|
||||
this.guild.gossipMessage(row)
|
||||
this._queueLinkEmbed(row).catch(() => {})
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
async _crosspostAnnouncement (sourceMsg, sourceCh) {
|
||||
if (!this.announcements || !this.guild?.guild) return []
|
||||
const targets = await this.announcements.getCrosspostTargets(
|
||||
sourceMsg.guildId,
|
||||
sourceMsg.channelId
|
||||
)
|
||||
if (!targets.length) return []
|
||||
const channels = await this.guild.listChannels()
|
||||
const body = formatCrosspostBody(sourceCh.name, sourceMsg.content)
|
||||
const out = []
|
||||
for (const targetId of targets) {
|
||||
if (targetId === sourceMsg.channelId) continue
|
||||
const tch = channels.find((c) => c.id === targetId)
|
||||
if (!tch || tch.type !== 'text') continue
|
||||
try {
|
||||
const row = await this._insertGuildMessage({
|
||||
guildId: sourceMsg.guildId,
|
||||
channelId: targetId,
|
||||
content: body,
|
||||
authorId: sourceMsg.authorId
|
||||
})
|
||||
out.push(row)
|
||||
} catch {
|
||||
// skip invalid target
|
||||
}
|
||||
}
|
||||
if (out.length) this.emit('message')
|
||||
return out
|
||||
}
|
||||
|
||||
async _notifyAnnouncementFollow ({ message, guildName, channelName, authorName }) {
|
||||
if (!this.notifications) return null
|
||||
const prefs = await this.notifications.getPrefs()
|
||||
if (prefs.notifyOnAnnouncement === false) return null
|
||||
if (prefs.mutedGuildIds?.includes(message.guildId)) return null
|
||||
if (prefs.mutedChannelIds?.includes(message.channelId)) return null
|
||||
if (this.activeChannelId === message.channelId) return null
|
||||
const record = {
|
||||
id: id(),
|
||||
userId: this.identity.user.id,
|
||||
kind: 'announcement',
|
||||
guildId: message.guildId,
|
||||
channelId: message.channelId,
|
||||
messageId: message.id,
|
||||
authorId: message.authorId,
|
||||
authorName,
|
||||
title: `📢 ${authorName} posted in #${channelName || 'announcements'}`,
|
||||
body: String(message.content || '').slice(0, 140),
|
||||
place: `#${channelName || 'announcements'} · ${guildName || 'Server'}`,
|
||||
read: false,
|
||||
createdAt: now()
|
||||
}
|
||||
return this.notifications.push(record)
|
||||
}
|
||||
|
||||
async setAnnouncementCrosspostTargets (sourceChannelId, targetChannelIds) {
|
||||
if (!this.guild?.guild) throw new Error('no guild')
|
||||
const roles = await this._memberRoles()
|
||||
if (!roleHasPermission(roles, PERMISSION.MANAGE_CHANNELS)) {
|
||||
throw new Error('no permission to configure cross-post')
|
||||
}
|
||||
await this._ensureGuildModules()
|
||||
const ch = await this.db.get(COLLECTIONS.CHANNELS, {
|
||||
guildId: this.guild.guild.id,
|
||||
id: sourceChannelId
|
||||
})
|
||||
if (!ch || !isAnnouncementChannel(ch)) throw new Error('not an announcement channel')
|
||||
const row = await this.announcements.setCrosspostTargets(
|
||||
this.guild.guild.id,
|
||||
sourceChannelId,
|
||||
targetChannelIds
|
||||
)
|
||||
this.guild.gossipAnnouncementCrosspost(row)
|
||||
this.emit('announcements', row)
|
||||
return row
|
||||
}
|
||||
|
||||
async getAnnouncementCrosspostTargets (sourceChannelId) {
|
||||
await this._ensureGuildModules()
|
||||
if (!this.guild?.guild) return []
|
||||
return this.announcements.getCrosspostTargets(
|
||||
this.guild.guild.id,
|
||||
sourceChannelId
|
||||
)
|
||||
}
|
||||
|
||||
async followAnnouncementChannel (channelId) {
|
||||
if (!this.guild?.guild || !this.identity?.user) throw new Error('no guild')
|
||||
const ch = await this.db.get(COLLECTIONS.CHANNELS, {
|
||||
guildId: this.guild.guild.id,
|
||||
id: channelId
|
||||
})
|
||||
if (!ch || !isAnnouncementChannel(ch)) throw new Error('not an announcement channel')
|
||||
await this._ensureGuildModules()
|
||||
const row = await this.announcements.follow(
|
||||
this.guild.guild.id,
|
||||
channelId,
|
||||
this.identity.user.id
|
||||
)
|
||||
this.guild.gossipAnnouncementFollow(row)
|
||||
this.emit('announcements', row)
|
||||
return row
|
||||
}
|
||||
|
||||
async unfollowAnnouncementChannel (channelId) {
|
||||
if (!this.guild?.guild || !this.identity?.user) throw new Error('no guild')
|
||||
await this._ensureGuildModules()
|
||||
const row = await this.announcements.unfollow(
|
||||
this.guild.guild.id,
|
||||
channelId,
|
||||
this.identity.user.id
|
||||
)
|
||||
this.guild.gossipAnnouncementFollow(row)
|
||||
this.emit('announcements', row)
|
||||
return row
|
||||
}
|
||||
|
||||
async sendMessage (content, opts = {}) {
|
||||
if (!this.messages) throw new Error('no channel')
|
||||
if (this.mode === 'guild' && parseSlashInvocation(content)) {
|
||||
@@ -2874,6 +3046,29 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.automod.setGuild(guild.id)
|
||||
automodConfig = await this.automod.getConfig()
|
||||
}
|
||||
let announcementCrosspostTargets = []
|
||||
let announcementCrosspostByChannel = {}
|
||||
let announcementFollowing = false
|
||||
let announcementFollows = []
|
||||
if (guild && this.announcements && user) {
|
||||
for (const ch of channels) {
|
||||
if (!isAnnouncementChannel(ch)) continue
|
||||
announcementCrosspostByChannel[ch.id] = await this.announcements.getCrosspostTargets(
|
||||
guild.id,
|
||||
ch.id
|
||||
)
|
||||
}
|
||||
if (activeChannel && isAnnouncementChannel(activeChannel)) {
|
||||
announcementCrosspostTargets =
|
||||
announcementCrosspostByChannel[activeChannel.id] || []
|
||||
announcementFollowing = await this.announcements.isFollowing(
|
||||
guild.id,
|
||||
activeChannel.id,
|
||||
user.id
|
||||
)
|
||||
}
|
||||
announcementFollows = await this.announcements.listMyFollows(user.id, guild.id)
|
||||
}
|
||||
return {
|
||||
onboarded: this.onboarded,
|
||||
user,
|
||||
@@ -2916,6 +3111,10 @@ class PearcordPlatform extends EventEmitter {
|
||||
myStageRole,
|
||||
stagePendingRequests,
|
||||
automodConfig,
|
||||
announcementCrosspostTargets,
|
||||
announcementCrosspostByChannel,
|
||||
announcementFollowing,
|
||||
announcementFollows,
|
||||
myVoiceChannelId,
|
||||
voiceMedia: this.voiceMedia?.snapshot() || null,
|
||||
screenShare: this.screenShare?.snapshot() || null,
|
||||
@@ -3042,6 +3241,7 @@ class PearcordPlatform extends EventEmitter {
|
||||
this.voice = null
|
||||
this.voiceMedia = null
|
||||
this.screenShare = null
|
||||
this.announcements = null
|
||||
this.removeAllListeners()
|
||||
await this.db.close()
|
||||
}
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@
|
||||
"pearcord-forum": "file:../pearcord-forum",
|
||||
"pearcord-stage": "file:../pearcord-stage",
|
||||
"pearcord-automod": "file:../pearcord-automod",
|
||||
"pearcord-screen-share": "git+https://git.ssh.surf/pearcord/pearcord-screen-share.git#main",
|
||||
"pearcord-screen-share": "file:../pearcord-screen-share",
|
||||
"pearcord-announcements": "git+https://git.ssh.surf/pearcord/pearcord-announcements.git#main",
|
||||
"hyperswarm": "^4.11.6",
|
||||
"pearcord-shared": "file:../pearcord-shared"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user