From c20a954cfcc1e5341df37009ff254806889c2831 Mon Sep 17 00:00:00 2001
From: Keyan <34140557+huumn@users.noreply.github.com>
Date: Tue, 23 Jul 2024 10:35:15 -0500
Subject: [PATCH] This day on ... automated post (#1273)
* add this day posting job
* put in proper timezone
* make sure we're in central timezone
* schedule thisDay job
---
api/resolvers/item.js | 2 +-
pages/day/[day].js | 215 ------------------
pages/day/index.js | 1 -
.../20240723151608_this_day/migration.sql | 16 ++
worker/index.js | 2 +
worker/thisDay.js | 187 +++++++++++++++
6 files changed, 206 insertions(+), 217 deletions(-)
delete mode 100644 pages/day/[day].js
delete mode 100644 pages/day/index.js
create mode 100644 prisma/migrations/20240723151608_this_day/migration.sql
create mode 100644 worker/thisDay.js
diff --git a/api/resolvers/item.js b/api/resolvers/item.js
index 51eaa460..5886cb3c 100644
--- a/api/resolvers/item.js
+++ b/api/resolvers/item.js
@@ -1328,7 +1328,7 @@ export const createItem = async (parent, { forward, ...item }, { me, models, lnd
return resultItem
}
-const getForwardUsers = async (models, forward) => {
+export const getForwardUsers = async (models, forward) => {
const fwdUsers = []
if (forward) {
// find all users in one db query
diff --git a/pages/day/[day].js b/pages/day/[day].js
deleted file mode 100644
index 75b9b6e6..00000000
--- a/pages/day/[day].js
+++ /dev/null
@@ -1,215 +0,0 @@
-import { getGetServerSideProps } from '@/api/ssrApollo'
-import Layout from '@/components/layout'
-import { datePivot, dayMonthYearToDate } from '@/lib/time'
-import { gql, useQuery } from '@apollo/client'
-import { numWithUnits, suffix, abbrNum } from '@/lib/format'
-import PageLoading from '@/components/page-loading'
-import { useRouter } from 'next/router'
-
-// force SSR to include CSP nonces
-export const getServerSideProps = getGetServerSideProps({ query: null })
-
-const THIS_DAY = gql`
- query thisDay($to: String, $from: String) {
- posts: items (sort: "top", when: "custom", from: $from, to: $to, limit: 1) {
- items {
- id
- title
- text
- url
- ncomments
- sats
- boost
- subName
- user {
- name
- }
- }
- }
- comments: items (sort: "top", type: "comments", when: "custom", from: $from, to: $to, limit: 1) {
- items {
- id
- parentId
- text
- ncomments
- sats
- boost
- user {
- name
- }
- root {
- title
- id
- subName
- user {
- name
- }
- }
- }
- }
- users: topUsers(when: "custom", from: $from, to: $to) {
- users {
- name
- nposts(when: "custom", from: $from, to: $to)
- ncomments(when: "custom", from: $from, to: $to)
- optional {
- stacked(when: "custom", from: $from, to: $to)
- spent(when: "custom", from: $from, to: $to)
- referrals(when: "custom", from: $from, to: $to)
- }
- }
- }
- territories: topSubs(when: "custom", from: $from, to: $to, limit: 1) {
- subs {
- name
- createdAt
- desc
- user {
- name
- id
- optional {
- streak
- }
- }
- ncomments(when: "custom", from: $from, to: $to)
- nposts(when: "custom", from: $from, to: $to)
-
- optional {
- stacked(when: "custom", from: $from, to: $to)
- spent(when: "custom", from: $from, to: $to)
- revenue(when: "custom", from: $from, to: $to)
- }
- }
- }
- }
-`
-
-export default function Index () {
- const router = useRouter()
- const days = []
- let day = router.query.day
- ? datePivot(dayMonthYearToDate(router.query.day), { years: -1 })
- : datePivot(new Date(), { years: -1 })
- while (day > new Date('2021-06-10')) {
- days.push(day)
- day = datePivot(day, { years: -1 })
- }
-
- const sep = `
-https://imgprxy.stacker.news/fsFoWlgwKYsk5mxx2ijgqU8fg04I_2zA_D28t_grR74/rs:fit:960:540/aHR0cHM6Ly9tLnN0YWNrZXIubmV3cy8yMzc5Ng
-`
-
- return (
-
-
- * * -
- {days
- .map(day => )
- .reduce((acc, x) => acc === null
- ? [x]
- : [acc, sep, x], null)}
-
-
- )
-}
-
-function ThisDay ({ day }) {
- const [from, to] = [
- String(new Date(new Date(day).setHours(0, 0, 0, 0)).getTime()),
- String(new Date(new Date(day).setHours(23, 59, 59, 999)).getTime())]
-
- const { data } = useQuery(THIS_DAY, { variables: { from, to } })
-
- if (!data) return
-
- return `
-### ${day.toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' })} π
-
-----
-
-### π \`TOP POST\`
-
-${topPost(data.posts.items)}
-
-----
-
-### π¬ \`TOP COMMENT\`
-
-${topComment(data.comments.items)}
-
-----
-
-### π \`TOP STACKER\`
-
-${topStacker(data.users.users)}
-
-----
-
-### πΊοΈ \`TOP TERRITORY\`
-
-${topTerritory(data.territories.subs)}
-`
-}
-
-const truncateString = (string = '', maxLength = 140) =>
- string.length > maxLength
- ? `${string.substring(0, 250)} [β¦]`
- : string
-
-function topPost (posts) {
- const post = posts?.[0]
-
- if (!post) return 'No top post'
-
- return `**[${post.title}](https://stacker.news/items/${post.id}/r/Undisciplined)**
-${post.text
-? `
-#### Excerpt
-> ${truncateString(post.text)}`
-: ''}
-*${numWithUnits(post.sats)} \\ ${numWithUnits(post.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ @${post.user.name} \\ ~${post.subName}*`
-}
-
-function topComment (comments) {
- const comment = comments?.[0]
-
- if (!comment) return 'No top comment'
-
- return `**https://stacker.news/items/${comment.root.id}/r/Undisciplined?commentId=${comment.id}**
-
-${comment.text
-? `
-#### Excerpt
-> ${truncateString(comment.text)}`
-: ''}
-
-*${numWithUnits(comment.sats)} \\ ${numWithUnits(comment.ncomments, { unitSingular: 'reply', unitPlural: 'replies' })} \\ @${comment.user.name}*
-
-From **[${comment.root.title}](https://stacker.news/items/${comment.root.id}/r/Undisciplined)** by @${comment.root.user.name} in ~${comment.root.subName}`
-}
-
-function topStacker (users) {
- const userIdx = users.findIndex(u => !!u)
-
- if (userIdx === -1) return 'No top stacker'
- const user = users[userIdx]
-
- return `${suffix(userIdx + 1)} place **@${user.name}** ${userIdx > 0 ? `(1st${userIdx > 1 ? `-${suffix(userIdx - 1)}` : ''} hiding)` : ''}
-
-*${abbrNum(user.optional?.stacked)} stacked \\ ${abbrNum(user.optional?.spent)} spent \\ ${numWithUnits(user.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(user.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ ${numWithUnits(user.optional.referrals, { unitSingular: 'referral', unitPlural: 'referrals' })}*`
-}
-
-function topTerritory (subs) {
- const sub = subs?.[0]
-
- if (!sub) return 'No top territory'
-
- return `**~${sub.name}**
-${sub.desc
-? `> ${truncateString(sub.desc)}`
-: ''}
-
-founded by @${sub.user.name} on ${new Date(sub.createdAt).toDateString()}
-
-*${abbrNum(sub.optional?.stacked)} stacked \\ ${abbrNum(sub.optional?.revenue)} revenue \\ ${abbrNum(sub.optional?.spent)} spent \\ ${numWithUnits(sub.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(sub.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })}*`
-}
diff --git a/pages/day/index.js b/pages/day/index.js
deleted file mode 100644
index 0526dd32..00000000
--- a/pages/day/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default, getServerSideProps } from './[day].js'
diff --git a/prisma/migrations/20240723151608_this_day/migration.sql b/prisma/migrations/20240723151608_this_day/migration.sql
new file mode 100644
index 00000000..25eade80
--- /dev/null
+++ b/prisma/migrations/20240723151608_this_day/migration.sql
@@ -0,0 +1,16 @@
+CREATE OR REPLACE FUNCTION schedule_this_day_job()
+RETURNS INTEGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+BEGIN
+ INSERT INTO pgboss.schedule (name, cron, timezone)
+ VALUES ('thisDay', '0 5 * * *', 'America/Chicago') ON CONFLICT DO NOTHING;
+ return 0;
+EXCEPTION WHEN OTHERS THEN
+ return 0;
+END;
+$$;
+
+SELECT schedule_this_day_job();
+DROP FUNCTION IF EXISTS schedule_this_day_job;
\ No newline at end of file
diff --git a/worker/index.js b/worker/index.js
index 738ac5a6..e856c295 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -26,6 +26,7 @@ import { autoWithdraw } from './autowithdraw.js'
import { saltAndHashEmails } from './saltAndHashEmails.js'
import { remindUser } from './reminder.js'
import { holdAction, settleAction, settleActionError } from './paidAction.js'
+import { thisDay } from './thisDay.js'
const { loadEnvConfig } = nextEnv
const { ApolloClient, HttpLink, InMemoryCache } = apolloClient
@@ -111,6 +112,7 @@ async function work () {
await boss.work('settleAction', jobWrapper(settleAction))
await boss.work('holdAction', jobWrapper(holdAction))
await boss.work('checkInvoice', jobWrapper(checkInvoice))
+ await boss.work('thisDay', jobWrapper(thisDay))
console.log('working jobs')
}
diff --git a/worker/thisDay.js b/worker/thisDay.js
new file mode 100644
index 00000000..2d4b5e64
--- /dev/null
+++ b/worker/thisDay.js
@@ -0,0 +1,187 @@
+import { datePivot } from '@/lib/time'
+import gql from 'graphql-tag'
+import { numWithUnits, abbrNum } from '@/lib/format'
+import { paidActions } from '@/api/paidAction'
+import { USER_ID } from '@/lib/constants'
+import { getForwardUsers } from '@/api/resolvers/item'
+
+export async function thisDay ({ models, apollo }) {
+ const days = []
+ let yearsAgo = 1
+ while (datePivot(new Date(), { years: -yearsAgo }) > new Date('2021-06-10')) {
+ const [{ from, to }] = await models.$queryRaw`
+ SELECT (date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') - ${`${yearsAgo} year`}::interval as from,
+ (date_trunc('day', (now() AT TIME ZONE 'America/Chicago')) AT TIME ZONE 'America/Chicago') - ${`${yearsAgo} year`}::interval + interval '1 day - 1 second' as to`
+
+ const { data } = await apollo.query({
+ query: THIS_DAY,
+ variables: { from: new Date(from).getTime().toString(), to: new Date(to).getTime().toString() }
+ })
+
+ days.push({
+ data,
+ day: new Date(from).toLocaleString('default', { timeZone: 'America/Chicago', month: 'long', day: 'numeric', year: 'numeric' })
+ })
+
+ yearsAgo++
+ }
+
+ const date = new Date().toLocaleString('default', { timeZone: 'America/Chicago', month: 'long', day: 'numeric' })
+
+ const text = `${topPosts(days)}
+${topStackers(days)}
+${topComments(days)}
+${topSubs(days)}`
+
+ const user = await models.user.findUnique({ where: { id: USER_ID.sn } })
+ const forward = days.map(({ data }) => data.users.users?.[0]?.name).filter(Boolean).map(name => ({ nym: name, pct: 10 }))
+ forward.push({ nym: 'Undisciplined', pct: 50 })
+ const forwardUsers = await getForwardUsers(models, forward)
+ await models.$transaction(async tx => {
+ const context = { tx, cost: BigInt(1), user, models }
+ const result = await paidActions.ITEM_CREATE.perform({
+ text, title: `This Day on SN: ${date}`, subName: 'meta', userId: USER_ID.sn, forwardUsers
+ }, context)
+ await paidActions.ITEM_CREATE.onPaid(result, context)
+ })
+}
+
+function topPosts (days) {
+ let text = '#### Top Posts'
+ for (const { day, data } of days) {
+ const post = data.posts.items?.[0]
+ if (post) {
+ text += `
+- [${post.title}](${process.env.NEXT_PUBLIC_URL}/items/${post.id})
+ - ${numWithUnits(post.sats)} \\ ${numWithUnits(post.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ @${post.user.name} \\ ~${post.subName} \\ \`${day}\``
+ } else {
+ text += `
+- no top post for \`${day}\``
+ }
+ }
+ return text
+}
+
+function topStackers (days) {
+ let text = '#### Top Stackers'
+ for (const { day, data } of days) {
+ const user = data.users.users?.[0]
+ if (user) {
+ text += `
+- @${user.name}
+ - ${abbrNum(user.optional?.stacked)} stacked \\ ${abbrNum(user.optional?.spent)} spent \\ ${numWithUnits(user.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(user.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ \`${day}\``
+ } else {
+ text += `
+- stacker is in hiding for \`${day}\``
+ }
+ }
+ return text
+}
+
+function topComments (days) {
+ let text = '#### Top Comments'
+ for (const { day, data } of days) {
+ const comment = data.comments.items?.[0]
+ if (comment) {
+ text += `
+- ${process.env.NEXT_PUBLIC_URL}/items/${comment.root.id}?commentId=${comment.id} on [${comment.root.title}](${process.env.NEXT_PUBLIC_URL}/items/${comment.root.id})
+ - ${numWithUnits(comment.sats)} \\ ${numWithUnits(comment.ncomments, { unitSingular: 'reply', unitPlural: 'replies' })} \\ @${comment.user.name} \\ \`${day}\`
+ > ${comment.text.trim().split('\n')[0]} [...]`
+ } else {
+ text += `
+- no top comment for \`${day}\``
+ }
+ }
+ return text
+}
+
+function topSubs (days) {
+ let text = '#### Top Territories'
+ for (const { day, data } of days) {
+ const sub = data.territories.subs?.[0]
+ if (sub) {
+ text += `
+- ~${sub.name}
+ - ${abbrNum(sub.optional?.stacked)} stacked \\ ${abbrNum(sub.optional?.revenue)} revenue \\ ${abbrNum(sub.optional?.spent)} spent \\ ${numWithUnits(sub.nposts, { unitSingular: 'post', unitPlural: 'posts' })} \\ ${numWithUnits(sub.ncomments, { unitSingular: 'comment', unitPlural: 'comments' })} \\ \`${day}\``
+ } else {
+ text += `
+- no top territory for \`${day}\``
+ }
+ }
+ return text
+}
+
+const THIS_DAY = gql`
+ query thisDay($to: String, $from: String) {
+ posts: items (sort: "top", when: "custom", from: $from, to: $to, limit: 1) {
+ items {
+ id
+ title
+ text
+ url
+ ncomments
+ sats
+ boost
+ subName
+ user {
+ name
+ }
+ }
+ }
+ comments: items (sort: "top", type: "comments", when: "custom", from: $from, to: $to, limit: 1) {
+ items {
+ id
+ parentId
+ text
+ ncomments
+ sats
+ boost
+ user {
+ name
+ }
+ root {
+ title
+ id
+ subName
+ user {
+ name
+ }
+ }
+ }
+ }
+ users: topUsers(when: "custom", from: $from, to: $to, limit: 1) {
+ users {
+ name
+ nposts(when: "custom", from: $from, to: $to)
+ ncomments(when: "custom", from: $from, to: $to)
+ optional {
+ stacked(when: "custom", from: $from, to: $to)
+ spent(when: "custom", from: $from, to: $to)
+ referrals(when: "custom", from: $from, to: $to)
+ }
+ }
+ }
+ territories: topSubs(when: "custom", from: $from, to: $to, limit: 1) {
+ subs {
+ name
+ createdAt
+ desc
+ user {
+ name
+ id
+ optional {
+ streak
+ }
+ }
+ ncomments(when: "custom", from: $from, to: $to)
+ nposts(when: "custom", from: $from, to: $to)
+
+ optional {
+ stacked(when: "custom", from: $from, to: $to)
+ spent(when: "custom", from: $from, to: $to)
+ revenue(when: "custom", from: $from, to: $to)
+ }
+ }
+ }
+ }
+`