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
This commit is contained in:
		
							parent
							
								
									7fd4f58e81
								
							
						
					
					
						commit
						c20a954cfc
					
				| @ -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
 | ||||
|  | ||||
| @ -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 ( | ||||
|     <Layout> | ||||
|       <code style={{ whiteSpace: 'pre-line' }}> | ||||
|         * * - | ||||
|         {days | ||||
|           .map(day => <ThisDay key={day} day={day} />) | ||||
|           .reduce((acc, x) => acc === null | ||||
|             ? [x] | ||||
|             : [acc, sep, x], null)} | ||||
|       </code> | ||||
|     </Layout> | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| 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 <PageLoading /> | ||||
| 
 | ||||
|   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' })}*` | ||||
| } | ||||
| @ -1 +0,0 @@ | ||||
| export { default, getServerSideProps } from './[day].js' | ||||
							
								
								
									
										16
									
								
								prisma/migrations/20240723151608_this_day/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								prisma/migrations/20240723151608_this_day/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| @ -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') | ||||
| } | ||||
|  | ||||
							
								
								
									
										187
									
								
								worker/thisDay.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								worker/thisDay.js
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ` | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user