automated social posting (#2022)
* social posting without message selection * message formatting and scheduled job * small cleanup
This commit is contained in:
parent
1921c253ba
commit
8a6b825659
@ -32,6 +32,12 @@ SLACK_CHANNEL_ID=
|
||||
LNAUTH_URL=http://localhost:3000/api/lnauth
|
||||
LNWITH_URL=http://localhost:3000/api/lnwith
|
||||
|
||||
# auto social poster
|
||||
TWITTER_POSTER_API_KEY=
|
||||
TWITTER_POSTER_API_KEY_SECRET=
|
||||
TWITTER_POSTER_ACCESS_TOKEN=
|
||||
TWITTER_POSTER_ACCESS_TOKEN_SECRET=
|
||||
|
||||
########################################
|
||||
# SNDEV STUFF WE PRESET #
|
||||
# which you can override in .env.local #
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -91,6 +91,7 @@
|
||||
"textarea-caret": "^3.1.0",
|
||||
"tldts": "^6.1.51",
|
||||
"tsx": "^4.19.1",
|
||||
"twitter-api-v2": "^1.22.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unzipper": "^0.12.3",
|
||||
"url-unshort": "^6.1.0",
|
||||
@ -19632,6 +19633,11 @@
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
||||
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
||||
},
|
||||
"node_modules/twitter-api-v2": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.22.0.tgz",
|
||||
"integrity": "sha512-KlcRL9vcBzjeS/PwxX33NziP+SHp9n35DOclKtpOmnNes7nNVnK7WG4pKlHfBqGrY5kAz/8J5ERS8DWkYOaiWw=="
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||
|
@ -96,6 +96,7 @@
|
||||
"textarea-caret": "^3.1.0",
|
||||
"tldts": "^6.1.51",
|
||||
"tsx": "^4.19.1",
|
||||
"twitter-api-v2": "^1.22.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"unzipper": "^0.12.3",
|
||||
"url-unshort": "^6.1.0",
|
||||
|
@ -0,0 +1,36 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AutoSocialPost" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"itemId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "AutoSocialPost_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AutoSocialPost_itemId_idx" ON "AutoSocialPost"("itemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AutoSocialPost_created_at_idx" ON "AutoSocialPost"("created_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AutoSocialPost" ADD CONSTRAINT "AutoSocialPost_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION schedule_social_poster_job()
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- every 60th minute
|
||||
INSERT INTO pgboss.schedule (name, cron, timezone)
|
||||
VALUES ('socialPoster', '*/60 * * * *', 'America/Chicago') ON CONFLICT DO NOTHING;
|
||||
return 0;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
return 0;
|
||||
END;
|
||||
$$;
|
||||
|
||||
SELECT schedule_social_poster_job();
|
||||
DROP FUNCTION IF EXISTS schedule_social_poster_job;
|
@ -599,6 +599,7 @@ model Item {
|
||||
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
|
||||
PollBlindVote PollBlindVote[]
|
||||
ItemUserAgg ItemUserAgg[]
|
||||
AutoSocialPost AutoSocialPost[]
|
||||
|
||||
@@index([uploadId])
|
||||
@@index([lastZapAt])
|
||||
@ -649,6 +650,18 @@ model ItemUserAgg {
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// record auto social posts so we can avoid duplicates
|
||||
model AutoSocialPost {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
itemId Int
|
||||
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([itemId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// this is a denomalized table that is used to make reply notifications
|
||||
// more efficient ... it is populated by a trigger when replies are created
|
||||
model Reply {
|
||||
|
@ -37,6 +37,7 @@ import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts'
|
||||
import { expireBoost } from './expireBoost'
|
||||
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
||||
import { autoDropBolt11s } from './autoDropBolt11'
|
||||
import { postToSocial } from './socialPoster'
|
||||
|
||||
// WebSocket polyfill
|
||||
import ws from 'isomorphic-ws'
|
||||
@ -142,6 +143,7 @@ async function work () {
|
||||
await boss.work('saltAndHashEmails', jobWrapper(saltAndHashEmails))
|
||||
await boss.work('reminder', jobWrapper(remindUser))
|
||||
await boss.work('thisDay', jobWrapper(thisDay))
|
||||
await boss.work('socialPoster', jobWrapper(postToSocial))
|
||||
|
||||
console.log('working jobs')
|
||||
}
|
||||
|
109
worker/socialPoster.js
Normal file
109
worker/socialPoster.js
Normal file
@ -0,0 +1,109 @@
|
||||
import Nostr from '@/lib/nostr'
|
||||
import { TwitterApi } from 'twitter-api-v2'
|
||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
|
||||
async function postToTwitter ({ message }) {
|
||||
if (!process.env.TWITTER_POSTER_API_KEY ||
|
||||
!process.env.TWITTER_POSTER_API_KEY_SECRET ||
|
||||
!process.env.TWITTER_POSTER_ACCESS_TOKEN ||
|
||||
!process.env.TWITTER_POSTER_ACCESS_TOKEN_SECRET) {
|
||||
console.log('Twitter poster not configured')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new TwitterApi({
|
||||
appKey: process.env.TWITTER_POSTER_API_KEY,
|
||||
appSecret: process.env.TWITTER_POSTER_API_KEY_SECRET,
|
||||
accessToken: process.env.TWITTER_POSTER_ACCESS_TOKEN,
|
||||
accessSecret: process.env.TWITTER_POSTER_ACCESS_TOKEN_SECRET
|
||||
})
|
||||
await client.appLogin()
|
||||
await client.v2.tweet(message)
|
||||
console.log('Successfully posted to Twitter')
|
||||
} catch (err) {
|
||||
console.error('Error posting to Twitter:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const RELAYS = [
|
||||
'wss://nos.lol/',
|
||||
'wss://nostr.land/',
|
||||
'wss://nostr.wine/',
|
||||
'wss://purplerelay.com/',
|
||||
'wss://relay.damus.io/',
|
||||
'wss://relay.snort.social/',
|
||||
'wss://relay.nostr.band/',
|
||||
'wss://relay.primal.net/'
|
||||
]
|
||||
|
||||
async function postToNostr ({ message }) {
|
||||
if (!process.env.NOSTR_PRIVATE_KEY) {
|
||||
console.log('Nostr poster not configured')
|
||||
return
|
||||
}
|
||||
|
||||
const nostr = Nostr.get()
|
||||
const signer = nostr.getSigner({ privKey: process.env.NOSTR_PRIVATE_KEY })
|
||||
try {
|
||||
await nostr.publish({
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
content: message,
|
||||
tags: [],
|
||||
kind: 1
|
||||
}, {
|
||||
relays: RELAYS,
|
||||
signer,
|
||||
timeout: 5000
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error posting to Nostr:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function getHottestItem ({ models }) {
|
||||
const item = await models.$queryRaw`
|
||||
SELECT "Item".*, users.name as "userName"
|
||||
FROM "Item"
|
||||
JOIN hot_score_view ON "Item"."id" = hot_score_view.id
|
||||
JOIN users ON "Item"."userId" = users.id
|
||||
LEFT JOIN "AutoSocialPost" ON "Item"."id" = "AutoSocialPost"."itemId"
|
||||
WHERE "AutoSocialPost"."id" IS NULL
|
||||
AND "Item"."parentId" IS NULL
|
||||
AND NOT "Item".bio
|
||||
AND "Item"."deletedAt" IS NULL
|
||||
ORDER BY "hot_score_view"."hot_score" DESC
|
||||
LIMIT 1`
|
||||
|
||||
if (item.length === 0) {
|
||||
console.log('No item to post')
|
||||
return null
|
||||
}
|
||||
|
||||
await models.AutoSocialPost.create({
|
||||
data: {
|
||||
itemId: item[0].id
|
||||
}
|
||||
})
|
||||
|
||||
return item[0]
|
||||
}
|
||||
|
||||
async function itemToMessage ({ item }) {
|
||||
return `${item.title}
|
||||
|
||||
by ${item.userName} to ~${item.subName}
|
||||
${numWithUnits(msatsToSats(item.msats), { abbreviate: false })} so far
|
||||
|
||||
https://stacker.news/items/${item.id}/r/sn`
|
||||
}
|
||||
|
||||
export async function postToSocial ({ models }) {
|
||||
const item = await getHottestItem({ models })
|
||||
if (item) {
|
||||
const message = await itemToMessage({ item })
|
||||
console.log('Message:', message)
|
||||
await postToTwitter({ message })
|
||||
await postToNostr({ message })
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user