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) + } + } + } + } +`