Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manager Stuff #26

Merged
merged 4 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config/example.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
"celebration": "CXXXX",
"void": "CXXX",
"approval": "DXXX",
"certification_approval": "DXXX"
"certification_approval": "DXXX",
"checkin": "CXXX"
},
"groups": {
"students": "SXXX"
},
"users": {
"copres": ["UXXX"],
"admins": ["UXXX"],
"devs": ["UXXX"]
}
Expand Down
2 changes: 1 addition & 1 deletion dev/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"oauth_config": {
"scopes": {
"bot": ["app_mentions:read", "channels:history", "channels:join", "channels:read", "chat:write", "chat:write.public", "commands", "files:write", "groups:read", "im:history", "im:read", "im:write", "mpim:read", "usergroups:read", "users:read", "users.profile:read", "users:read.email"]
"bot": ["app_mentions:read", "channels:history", "channels:join", "channels:read", "chat:write", "chat:write.public", "commands", "files:write", "groups:read", "im:history", "im:read", "im:write", "mpim:read", "mpim:write", "reactions:write", "usergroups:read", "users.profile:read", "users:read", "users:read.email", "mpim:write.topic"]
}
},
"settings": {
Expand Down
4 changes: 2 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export default tseslint.config(
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
caughtErrorsIgnorePattern: '^_'
}
],
'prefer-const': 'warn',
'@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': 'allow-with-description' }]
Expand Down
2 changes: 1 addition & 1 deletion src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const transport = pino.transport({
]
})

const logger = pino({ level: 'trace' }, transport)
const logger = pino({ level: 'debug' }, transport)

const pinoToBoltLevel: Record<LevelWithSilentOrString, BoltLogLevel> = {
fatal: BoltLogLevel.ERROR,
Expand Down
7 changes: 0 additions & 7 deletions src/lib/sockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ export function startWS(server: HttpServer) {
path: '/ws'
})
logger.info('Websocket server started')

io.on('connection', (socket) => {
socket.emit('hello', 'world')
socket.on('hello', (data) => {
socket.broadcast.emit('hello', data)
})
})
}

export function emitCluckChange(data: WSCluckChange) {
Expand Down
15 changes: 13 additions & 2 deletions src/routes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ router.get('/members', requireReadAPI, async (c) => {
use_slack_photo: true,
slack_photo: true,
slack_photo_small: true,
fallback_photo: true
fallback_photo: true,
MemberCerts: {
where: {
Cert: {
isManager: true
}
},
select: {
cert_id: true
}
}
},
where: {
active: true
Expand All @@ -34,7 +44,8 @@ router.get('/members', requireReadAPI, async (c) => {
first_name: member.first_name,
full_name: member.full_name,
photo: getMemberPhotoOrDefault(member, false),
photo_small: getMemberPhotoOrDefault(member, true)
photo_small: getMemberPhotoOrDefault(member, true),
isManager: member.MemberCerts.length > 0
}))
return c.json(resp)
})
Expand Down
2 changes: 1 addition & 1 deletion src/slack/blocks/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default {
autoSignoutDM(v: { slack_id: string; time_in: Date }) {
return Message()
.text(
`Hey <@${v.slack_id}>! You signed into the lab today at ${v.time_in.toLocaleTimeString()} but forgot to sign out, so we didn't log your hours for today :( Make sure you always sign out before you leave. Hope you had fun and excited to see you in the lab again!`
`Hey <@${v.slack_id}>! You signed into the lab today at ${v.time_in.toLocaleTimeString('en-us', { hour: 'numeric', hour12: true, minute: '2-digit' })} but forgot to sign out, so we didn't log your hours for today :( Make sure you always sign out before you leave. Hope you had fun and excited to see you in the lab again!`
rutmanz marked this conversation as resolved.
Show resolved Hide resolved
)
.buildToObject()
}
Expand Down
44 changes: 44 additions & 0 deletions src/slack/handlers/actions/checkin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import config from '~lib/config'
import logger from '~lib/logger'
import prisma from '~lib/prisma'
import { EventMiddleware } from '~slack/lib/types'

export const handleAppMentioned: EventMiddleware<'app_mention'> = async ({ event, client }) => {
if (event.channel == config.slack.channels.checkin) {
await client.reactions.add({
channel: event.channel,
timestamp: event.ts,
name: 'stopwatch'
})
const user = await client.auth.test()
const managers = await prisma.member.findMany({ where: { MemberCerts: { some: { Cert: { isManager: true } } } } })
const copres_string = config.slack.users.copres.join(',')
for (const manager of managers) {
if (!manager.slack_id) {
logger.warn('No slack id for manager ' + manager.email)
continue
}
const dm = await client.conversations.open({
users: copres_string + ',' + manager.slack_id
})
if (dm.channel?.id == null) {
logger.warn('No group dm for manager ' + manager.email)
continue
}
await client.chat.postMessage({
channel: dm.channel!.id!,
text: event.text.replace(user.user_id!, manager.slack_id)
})
}
await client.reactions.remove({
channel: event.channel,
timestamp: event.ts,
name: 'stopwatch'
})
await client.reactions.add({
channel: event.channel,
timestamp: event.ts,
name: 'white_check_mark'
})
}
}
2 changes: 2 additions & 0 deletions src/slack/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
handleSubmitHoursRejectModal
} from './actions/hours_response'
import { handleRunTask } from '~slack/handlers/actions/run_task'
import { handleAppMentioned } from './actions/checkin'

export enum ActionIDs {
ACCEPT = 'accept',
Expand Down Expand Up @@ -91,6 +92,7 @@ export function registerSlackHandlers(app: App) {
app.view(ViewIDs.MODAL_ONBOARDING, handleSubmitOnboardingModal)
// Events
app.event('app_home_opened', handleAppHomeOpened)
app.event('app_mention', handleAppMentioned)
app.action(/./, async ({ body, logger, action }) => {
const details: Record<string, string> = {
type: body?.type,
Expand Down
49 changes: 49 additions & 0 deletions src/tasks/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Prisma } from '@prisma/client'
import config from '~lib/config'
import logger from '~lib/logger'
import prisma from '~lib/prisma'
import { emitCluckChange } from '~lib/sockets'
import { slack_client } from '~slack'
import responses from '~slack/blocks/responses'

export async function promptCheckinMessage() {
await slack_client.chat.postMessage({
channel: config.slack.channels.checkin,
text: "<!channel> it's that time again! Make a checkin post"
})
}

export async function logoutAll() {
const loggedIn = await prisma.hourLog.findMany({
where: {
state: 'pending',
type: 'lab'
},
select: { time_in: true, Member: { select: { slack_id: true, email: true } } }
})
for (const log of loggedIn) {
try {
const slack_id = log.Member.slack_id
emitCluckChange({ email: log.Member.email, logging_in: false })
if (slack_id) {
await slack_client.chat.postMessage({
...responses.autoSignoutDM({ slack_id, time_in: log.time_in }),
channel: slack_id
})
}
} catch (e) {
logger.warn(e)
}
}
await prisma.hourLog.updateMany({
where: {
state: 'pending',
type: 'lab'
},
data: {
state: 'cancelled',
time_out: new Date(),
duration: new Prisma.Decimal(0)
}
})
}
20 changes: 15 additions & 5 deletions src/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import { syncSlackMembers } from '~tasks/slack'
import { announceNewCerts, updateProfileCerts } from '~tasks/certs'
import { updateSheet } from '~spreadsheet'
import { syncFallbackPhotos } from './photos'
import { setupAutoLogout } from './midnight'
import schedule from 'node-schedule'
import { logoutAll, promptCheckinMessage } from './calendar'

type TaskFunc = (reason: string) => Promise<void>
type TaskFunc = ((reason: string) => Promise<void>) & { label: string }
type Func = (() => void) | (() => Promise<void>)

const tasks: Record<string, TaskFunc> = {}

function createTaskFunc(task: Func): TaskFunc {
const label = 'task/' + task.name
return async (reason: string) => {
const func = async (reason: string) => {
try {
await task()
} catch (e) {
Expand All @@ -23,6 +24,8 @@ function createTaskFunc(task: Func): TaskFunc {
logger.info({ name: label }, 'Task ran successfully')
return
}
func.label = label
return func
}
function scheduleTask(task: Func, interval_seconds: number, runOnInit: boolean, offset_seconds: number): TaskFunc {
const cb = createTaskFunc(task)
Expand All @@ -39,18 +42,25 @@ function scheduleTask(task: Func, interval_seconds: number, runOnInit: boolean,
return cb
}

function scheduleCronTask(task: TaskFunc, cron_exp: string) {
schedule.scheduleJob(task.label, cron_exp, (_date) => task('scheduled run'))
return task
}

export function scheduleTasks() {
// Offset is to combat Slack's rate limits
const isProd = process.env.NODE_ENV === 'prod'

tasks['Sync Sheet'] = scheduleTask(updateSheet, 60 * 5, isProd, 0)
tasks['Announce Certs'] = scheduleTask(announceNewCerts, 60 * 60, isProd, 60) // Just in case the cert announcement isn't automatically run on changes
tasks['Sync Usergroups'] = scheduleTask(updateSlackUsergroups, 60 * 60, isProd, 2 * 60)
tasks['Update Profile Certs'] = scheduleTask(updateProfileCerts, 60 * 60 * 24, isProd, 5 * 60)
tasks['Link Fallback Photos'] = createTaskFunc(syncFallbackPhotos)
setupAutoLogout()
tasks['Logout All'] = scheduleCronTask(createTaskFunc(logoutAll), '0 0 * * *')

// Slack is silly and can only handle 5 items in the overflow menu
scheduleCronTask(createTaskFunc(promptCheckinMessage), '0 9 * * SAT')
scheduleTask(syncSlackMembers, 60 * 60, isProd, 0) // can be run from the admin members page
scheduleTask(updateProfileCerts, 60 * 60 * 24, isProd, 5 * 60)
}

export async function runTask(key: string) {
Expand Down
42 changes: 0 additions & 42 deletions src/tasks/midnight.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type APIMember = {
full_name: string
photo: string
photo_small: string
isManager: boolean
}

export type APIClockLabRequest = {
Expand Down
3 changes: 3 additions & 0 deletions src/views/grid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export async function buildGrid() {
// Init button
const memberButton = document.createElement('div')
memberButton.classList.add('memberButton')
if (member.isManager) {
memberButton.classList.add('manager')
}
memberButton.id = member.email

// Set click toggle
Expand Down
10 changes: 10 additions & 0 deletions src/views/grid/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ box-shadow: inset 0 0 0 1000px rgba(0, 255, 85, 0.2); */
}
}

.memberButton.manager[data-loggedin='true'] {
box-shadow:
inset 0 0 0 1000px rgba(255, 255, 255, 0),
0 0 15px 7px rgb(251, 0, 255);
}

body[data-gridstyle='void'] {
.memberButton {
.buttonText {
Expand All @@ -124,6 +130,10 @@ body[data-gridstyle='void'] {
}
}

.manager .buttonText {
box-shadow: inset 0 0 0 1000px rgba(255, 70, 237, 0.6);
}

.buttonText {
user-select: none;
box-shadow: inset 0 0 0 1000px rgba(70, 255, 169, 0.6);
Expand Down
2 changes: 1 addition & 1 deletion src/views/grid/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ if (gridstyle == 'void') {

export function applyRandomStyles(e: HTMLDivElement) {
e.classList.add(randomChoice('labelLeft', 'labelRight', 'labelCenter'))
e.classList.add(randomChoice('labelTop', 'labelBottom'))
// e.classList.add(randomChoice('labelTop', 'labelBottom'))
e.style.fontFamily = randomChoice('gilroy', 'cocogoose', 'tcm')
}

Expand Down