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
|
LNAUTH_URL=http://localhost:3000/api/lnauth
|
||||||
LNWITH_URL=http://localhost:3000/api/lnwith
|
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 #
|
# SNDEV STUFF WE PRESET #
|
||||||
# which you can override in .env.local #
|
# 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",
|
"textarea-caret": "^3.1.0",
|
||||||
"tldts": "^6.1.51",
|
"tldts": "^6.1.51",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.1",
|
||||||
|
"twitter-api-v2": "^1.22.0",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"unzipper": "^0.12.3",
|
"unzipper": "^0.12.3",
|
||||||
"url-unshort": "^6.1.0",
|
"url-unshort": "^6.1.0",
|
||||||
@ -19632,6 +19633,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz",
|
||||||
"integrity": "sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw=="
|
"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": {
|
"node_modules/type": {
|
||||||
"version": "2.7.3",
|
"version": "2.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz",
|
||||||
|
@ -96,6 +96,7 @@
|
|||||||
"textarea-caret": "^3.1.0",
|
"textarea-caret": "^3.1.0",
|
||||||
"tldts": "^6.1.51",
|
"tldts": "^6.1.51",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.1",
|
||||||
|
"twitter-api-v2": "^1.22.0",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"unzipper": "^0.12.3",
|
"unzipper": "^0.12.3",
|
||||||
"url-unshort": "^6.1.0",
|
"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)
|
invoice Invoice? @relation(fields: [invoiceId], references: [id], onDelete: SetNull)
|
||||||
PollBlindVote PollBlindVote[]
|
PollBlindVote PollBlindVote[]
|
||||||
ItemUserAgg ItemUserAgg[]
|
ItemUserAgg ItemUserAgg[]
|
||||||
|
AutoSocialPost AutoSocialPost[]
|
||||||
|
|
||||||
@@index([uploadId])
|
@@index([uploadId])
|
||||||
@@index([lastZapAt])
|
@@index([lastZapAt])
|
||||||
@ -649,6 +650,18 @@ model ItemUserAgg {
|
|||||||
@@index([createdAt])
|
@@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
|
// this is a denomalized table that is used to make reply notifications
|
||||||
// more efficient ... it is populated by a trigger when replies are created
|
// more efficient ... it is populated by a trigger when replies are created
|
||||||
model Reply {
|
model Reply {
|
||||||
|
@ -37,6 +37,7 @@ import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts'
|
|||||||
import { expireBoost } from './expireBoost'
|
import { expireBoost } from './expireBoost'
|
||||||
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
||||||
import { autoDropBolt11s } from './autoDropBolt11'
|
import { autoDropBolt11s } from './autoDropBolt11'
|
||||||
|
import { postToSocial } from './socialPoster'
|
||||||
|
|
||||||
// WebSocket polyfill
|
// WebSocket polyfill
|
||||||
import ws from 'isomorphic-ws'
|
import ws from 'isomorphic-ws'
|
||||||
@ -142,6 +143,7 @@ async function work () {
|
|||||||
await boss.work('saltAndHashEmails', jobWrapper(saltAndHashEmails))
|
await boss.work('saltAndHashEmails', jobWrapper(saltAndHashEmails))
|
||||||
await boss.work('reminder', jobWrapper(remindUser))
|
await boss.work('reminder', jobWrapper(remindUser))
|
||||||
await boss.work('thisDay', jobWrapper(thisDay))
|
await boss.work('thisDay', jobWrapper(thisDay))
|
||||||
|
await boss.work('socialPoster', jobWrapper(postToSocial))
|
||||||
|
|
||||||
console.log('working jobs')
|
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