45 changed files with 1538 additions and 74 deletions
@ -1,23 +1,85 @@ |
|||||
# DTP Chat |
# DTP Chat |
||||
|
|
||||
A no-nonsense/no-frills communications platform. |
A no-nonsense/no-frills communications platform. |
||||
|
|
||||
## Production Host Configuration |
## System Requirements |
||||
|
|
||||
|
Make sure you've got the latest/greatest for your Ubuntu. |
||||
|
|
||||
|
```sh |
||||
|
apt -y update && apt -y upgrade |
||||
|
apt -y install python3-pip build-essential ffmpeg supervisor |
||||
``` |
``` |
||||
adduser dtp |
The Linux headers and image installs are optional. If you not using a GPU for video encoding, you won't need kernel sources to build your GPU's driver. |
||||
|
|
||||
|
```sh |
||||
|
apt -y install linux-headers-generic linux-headers-virtual linux-image-virtual linux-virtual |
||||
``` |
``` |
||||
|
|
||||
|
The latest pnpm instructions can always be found here: |
||||
|
[pnpm installation](https://pnpm.io/installation) |
||||
|
|
||||
|
These are just convenience copies from that page. |
||||
|
|
||||
|
```sh |
||||
|
corepack enable pnpm |
||||
|
corepack use pnpm@latest |
||||
``` |
``` |
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash |
```sh |
||||
|
pnpm install |
||||
``` |
``` |
||||
|
|
||||
### Install Yarn v2 |
## Development Host Management |
||||
|
|
||||
|
For convenience, it's possible to open multiple terminals in VS Code using `Ctrl + Shift + ~` |
||||
|
|
||||
|
In one terminal, start the workers and MinIO. |
||||
|
|
||||
|
```sh |
||||
|
./start-local |
||||
``` |
``` |
||||
corepack enable |
|
||||
yarn set version stable |
In a separate terminal, starts the dev application environment. |
||||
yarn install |
|
||||
|
```sh |
||||
|
pnpm dev |
||||
``` |
``` |
||||
|
|
||||
|
## Production Host Configuration |
||||
|
|
||||
|
Configure the firewall: |
||||
|
|
||||
|
```sh |
||||
|
ufw allow ssh |
||||
|
ufw allow http |
||||
|
ufw allow https |
||||
|
ufw allow from [console_ip] proto tcp to [host_ip] port 8190 |
||||
|
ufw enable |
||||
|
``` |
||||
|
Create the DTP admin user on the host. All production hosts require this user. Do not set a login password for this user. We will be creating SSH keys for the user to access your git repo as a deployer, but you will never log into the host using the DTP user account. |
||||
|
|
||||
|
Instead, we always log in as root, and use `su - dtp` to become the DTP user. |
||||
|
|
||||
|
```sh |
||||
|
adduser dtp # just accept the defaults or enter whatever you want |
||||
|
su - dtp # this will put you in the DTP home directory as the DTP user |
||||
|
ssh-keygen # generate the user's SSH key to use for git deployments |
||||
|
|
||||
|
# print the DTP user's SSH public key to provide to git repo as deploy key |
||||
|
cat ~/.ssh/id_rsa.pub |
||||
|
``` |
||||
|
Add that SSH key to your git repo as a deploy key. |
||||
|
|
||||
|
In the DTP user's home directory: |
||||
|
|
||||
|
```sh |
||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash |
||||
|
|
||||
|
npm install --lts |
||||
|
``` |
||||
|
|
||||
|
Edit `.bashrc` and set `NODE_ENV=production` |
||||
|
|
||||
## Emoji Picker |
## Emoji Picker |
||||
|
|
||||
Chat currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. |
Chat currently uses [emoji-picker-element](https://www.npmjs.com/package/emoji-picker-element) as the renderer of an emoji picker, and then it manages the presentation of the picker itself. |
@ -0,0 +1,66 @@ |
|||||
|
// admin.js
|
||||
|
// Copyright (C) 2024 Digital Telepresence, LLC
|
||||
|
// All Rights Reserved
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import path, { dirname } from 'path'; |
||||
|
import { fileURLToPath } from 'url'; |
||||
|
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
||||
|
|
||||
|
import mongoose from 'mongoose'; |
||||
|
const User = mongoose.model('User'); |
||||
|
const ChatRoom = mongoose.model('ChatRoom'); |
||||
|
const ChatMessage = mongoose.model('ChatMessage'); |
||||
|
const ChatImage = mongoose.model('Image'); |
||||
|
const Video = mongoose.model('Video'); |
||||
|
|
||||
|
import express from 'express'; |
||||
|
|
||||
|
import { SiteController, SiteError } from '../../lib/site-lib.js'; |
||||
|
|
||||
|
export default class AdminController extends SiteController { |
||||
|
|
||||
|
static get name ( ) { return 'AdminController'; } |
||||
|
static get slug ( ) { return 'admin'; } |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, AdminController); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const router = express.Router(); |
||||
|
this.dtp.app.use('/admin', router); |
||||
|
|
||||
|
router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user.js'))); |
||||
|
|
||||
|
router.get( |
||||
|
'/', |
||||
|
this.getDashboard.bind(this), |
||||
|
); |
||||
|
|
||||
|
return router; |
||||
|
} |
||||
|
|
||||
|
async getDashboard (req, res, next) { |
||||
|
const { user: userService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.currentView = 'admin'; |
||||
|
|
||||
|
res.locals.stats = { |
||||
|
userCount: await User.estimatedDocumentCount(), |
||||
|
chatRoomCount: await ChatRoom.estimatedDocumentCount(), |
||||
|
chatMessageCount: await ChatMessage.estimatedDocumentCount(), |
||||
|
imageCount: await ChatImage.estimatedDocumentCount(), |
||||
|
videoCount: await Video.estimatedDocumentCount(), |
||||
|
}; |
||||
|
|
||||
|
res.locals.latestSignups = await userService.getLatestSignups(10); |
||||
|
|
||||
|
res.render('admin/dashboard'); |
||||
|
} catch (error) { |
||||
|
this.error.log('failed to present the admin dashboard', { error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,70 @@ |
|||||
|
// admin/user.js
|
||||
|
// Copyright (C) 2024 Digital Telepresence, LLC
|
||||
|
// All Rights Reserved
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import express from 'express'; |
||||
|
|
||||
|
import { SiteController, SiteError } from '../../../lib/site-lib.js'; |
||||
|
|
||||
|
export default class UserAdminController extends SiteController { |
||||
|
|
||||
|
static get name ( ) { return 'UserAdminController'; } |
||||
|
static get slug ( ) { return 'admin'; } |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, UserAdminController); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const router = express.Router(); |
||||
|
|
||||
|
router.param('userId', this.populateUserId.bind(this)); |
||||
|
|
||||
|
router.get( |
||||
|
'/:userId', |
||||
|
this.getUserView.bind(this), |
||||
|
); |
||||
|
|
||||
|
router.get( |
||||
|
'/', |
||||
|
this.getDashboard.bind(this), |
||||
|
); |
||||
|
|
||||
|
return router; |
||||
|
} |
||||
|
|
||||
|
async populateUserId (req, res, next, userId) { |
||||
|
const { user: userService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.userAccount = await userService.getUserAccount(userId); |
||||
|
if (!res.locals.userAccount) { |
||||
|
throw new SiteError(404, 'User not found'); |
||||
|
} |
||||
|
return next(); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to populate user account', { userId, error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async getUserView (req, res) { |
||||
|
res.render('admin/user/view'); |
||||
|
} |
||||
|
|
||||
|
async getDashboard (req, res, next) { |
||||
|
const { user: userService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.currentView = 'admin'; |
||||
|
res.locals.adminView = 'user'; |
||||
|
|
||||
|
res.locals.latestSignups = await userService.getLatestSignups(10); |
||||
|
|
||||
|
res.render('admin/user/dashboard'); |
||||
|
} catch (error) { |
||||
|
this.error.log('failed to present the admin dashboard', { error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
extends layout/main |
||||
|
block admin-content |
||||
|
|
||||
|
include user/components/list-table |
||||
|
|
||||
|
mixin renderStatsBlock (label, value) |
||||
|
.stats-block.uk-text-center |
||||
|
.stat-label= label |
||||
|
.stat-value= value |
||||
|
|
||||
|
.uk-margin-medium |
||||
|
div(uk-grid) |
||||
|
.uk-width-1-5 |
||||
|
+renderStatsBlock('Users', formatCount(stats.userCount)) |
||||
|
.uk-width-1-5 |
||||
|
+renderStatsBlock('Chat Rooms', formatCount(stats.chatRoomCount)) |
||||
|
.uk-width-1-5 |
||||
|
+renderStatsBlock('Chat Messages', formatCount(stats.chatMessageCount)) |
||||
|
.uk-width-1-5 |
||||
|
+renderStatsBlock('Images', formatCount(stats.imageCount)) |
||||
|
.uk-width-1-5 |
||||
|
+renderStatsBlock('Videos', formatCount(stats.videoCount)) |
||||
|
|
||||
|
h2.uk-margin-small Latest Signups |
||||
|
+renderAdminUserTable(latestSignups) |
@ -0,0 +1,28 @@ |
|||||
|
extends ../../layout/main |
||||
|
block view-content |
||||
|
|
||||
|
.uk-margin |
||||
|
.uk-container.uk-container-expand |
||||
|
div(uk-grid) |
||||
|
div(style="width: 150px;") |
||||
|
ul.uk-nav.uk-nav-default |
||||
|
li.uk-nav-header Admin Menu |
||||
|
li |
||||
|
a(href="/admin").uk-display-block |
||||
|
.uk-flex |
||||
|
.uk-width-auto |
||||
|
.menu-icon |
||||
|
i.fa-solid.fa-home |
||||
|
.uk-width-expand |
||||
|
span Home |
||||
|
li |
||||
|
a(href="/admin/user").uk-display-block |
||||
|
.uk-flex |
||||
|
.uk-width-auto |
||||
|
.menu-icon |
||||
|
i.fa-solid.fa-user |
||||
|
.uk-width-expand |
||||
|
span Users |
||||
|
|
||||
|
.uk-width-expand |
||||
|
block admin-content |
@ -0,0 +1,19 @@ |
|||||
|
mixin renderAdminUserTable (users) |
||||
|
.uk-overflow-auto |
||||
|
table.uk-table.uk-table-small.uk-table-justify |
||||
|
thead |
||||
|
tr |
||||
|
th Username |
||||
|
th Display Name |
||||
|
th Email |
||||
|
th(uk-tooltip="(A)dmin (M)oderator (E)mailVerify (G)Cloaked Can(L)ogin Can(C)hat CanC(O)mment Can(R)eport ") Flags |
||||
|
th Created |
||||
|
tbody |
||||
|
each user in users |
||||
|
tr |
||||
|
td |
||||
|
a(href=`/admin/user/${user._id}`)= user.username |
||||
|
td= user.displayName || '---' |
||||
|
td= user.email |
||||
|
td.uk-text-fixed= getUserFlags(user) |
||||
|
td= dayjs(user.created).fromNow() |
@ -0,0 +1,7 @@ |
|||||
|
extends ../layout/main |
||||
|
block admin-content |
||||
|
|
||||
|
include components/list-table |
||||
|
|
||||
|
h1 Latest Signups |
||||
|
+renderAdminUserTable(latestSignups) |
@ -0,0 +1,103 @@ |
|||||
|
extends ../layout/main |
||||
|
block admin-content |
||||
|
|
||||
|
include ../../user/components/profile-picture |
||||
|
|
||||
|
.uk-card.uk-card-default.uk-card-small.uk-border-rounded |
||||
|
.uk-card-header |
||||
|
div(uk-grid).uk-grid-small |
||||
|
.uk-width-auto |
||||
|
+renderProfilePicture(userAccount) |
||||
|
|
||||
|
.uk-width-expand |
||||
|
.uk-margin |
||||
|
.uk-text-lead= user.displayName |
||||
|
div(uk-grid).uk-grid-small.uk-grid-divider |
||||
|
.uk-width-auto @#{user.username} |
||||
|
.uk-width-auto= user.email |
||||
|
.uk-width-auto Created #{dayjs(userAccount.created).fromNow()} on #{dayjs(userAccount.created).format('MMMM D, YYYY')} |
||||
|
|
||||
|
.uk-card-body |
||||
|
.uk-margin |
||||
|
label(for="profile-bio").uk-form-label bio |
||||
|
#profile-bio.markdown-block!= marked.parse(userAccount.bio || '(no bio provided)', { renderer: fullMarkdownRenderer }) |
||||
|
|
||||
|
.uk-margin |
||||
|
label(for="profile-badges").uk-form-label Profile Badges |
||||
|
input(id="profile-badges", type= "text", placeholder= "Comma-separated list of badge names", value= userAccount.badges.join(',')).uk-input |
||||
|
|
||||
|
.uk-margin |
||||
|
form(method="POST", action=`/admin/user/${userAccount._id}`).uk-form |
||||
|
.uk-margin |
||||
|
label.uk-form-label Flags |
||||
|
.uk-margin-small |
||||
|
div(uk-grid).uk-grid-small |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="isAdmin", checked= userAccount.flags.isAdmin) |
||||
|
.state.p-success |
||||
|
label Admin |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="isModerator", checked= userAccount.flags.isModerator) |
||||
|
.state.p-success |
||||
|
label Moderator |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="isEmailVerified", checked= userAccount.flags.isEmailVerified) |
||||
|
.state.p-success |
||||
|
label Email Verified |
||||
|
|
||||
|
.uk-margin |
||||
|
label.uk-form-label Permissions |
||||
|
.uk-margin-small |
||||
|
div(uk-grid).uk-grid-small |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="canLogin", checked= userAccount.permissions.canLogin) |
||||
|
.state.p-success |
||||
|
label Can Login |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="canChat", checked= userAccount.permissions.canChat) |
||||
|
.state.p-success |
||||
|
label Can Chat |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="canReport", checked= userAccount.permissions.canReport) |
||||
|
.state.p-success |
||||
|
label Can Report |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="canShareLinks", checked= userAccount.permissions.canShareLinks) |
||||
|
.state.p-success |
||||
|
label Can Share Links |
||||
|
|
||||
|
.uk-margin |
||||
|
label.uk-form-label Email Opt-In |
||||
|
.uk-margin-small |
||||
|
div(uk-grid).uk-grid-small |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="optIn.system", checked= userAccount.optIn.system) |
||||
|
.state.p-success |
||||
|
label System Messages |
||||
|
.uk-width-auto |
||||
|
.pretty.p-default |
||||
|
input(type="checkbox", name="optIn.marketing", checked= userAccount.optIn.marketing) |
||||
|
.state.p-success |
||||
|
label Marketing |
||||
|
|
||||
|
.uk-card-footer |
||||
|
div(uk-grid).uk-grid-medium |
||||
|
.uk-width-auto |
||||
|
button(type="submit", name="action", value="update").uk-button.uk-button-primary.uk-border-rounded |
||||
|
span |
||||
|
i.fa-solid.fa-save |
||||
|
span.uk-margin-small-left Update User |
||||
|
|
||||
|
.uk-width-auto |
||||
|
button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded |
||||
|
span |
||||
|
i.fa-solid.fa-cancel |
||||
|
span.uk-margin-small-left Ban User |
@ -0,0 +1,3 @@ |
|||||
|
include ../../components/library |
||||
|
include reaction-bar |
||||
|
+renderReactionBar(message) |
@ -0,0 +1,18 @@ |
|||||
|
mixin renderReactionBar (message) |
||||
|
.message-reaction-bar |
||||
|
if Array.isArray(message.reactions) && (message.reactions.length > 0) |
||||
|
.uk-flex.uk-flex-middle |
||||
|
each reaction of message.reactions |
||||
|
.uk-width-auto |
||||
|
button( |
||||
|
type="button", |
||||
|
data-message-id= message._id, |
||||
|
data-emoji= reaction.emoji, |
||||
|
onclick="return dtp.app.toggleMessageReaction(event);", |
||||
|
).message-react-button |
||||
|
.uk-flex.uk-flex-middle |
||||
|
span.reaction-emoji= reaction.emoji |
||||
|
span= formatCount(reaction.users.length) |
||||
|
div(uk-dropdown="mode: hover") |
||||
|
span.uk-margin-small-right= reaction.emoji |
||||
|
span= reaction.users.map((user) => user.username).join(',') |
@ -1,8 +1,27 @@ |
|||||
//- Common routines for all views |
//- Common routines for all views |
||||
- |
- |
||||
|
function formatCount (count) { |
||||
|
return numeral(count).format((count > 1000) ? '0,0.0a' : '0,0'); |
||||
|
} |
||||
|
|
||||
function getUserPictureUrl (userProfile, which) { |
function getUserPictureUrl (userProfile, which) { |
||||
if (!userProfile || !userProfile.picture || !userProfile.picture[which]) { |
if (!userProfile || !userProfile.picture || !userProfile.picture[which]) { |
||||
return `https://${site.domain}/img/default-member.png`; |
return `https://${site.domain}/img/default-member.png`; |
||||
} |
} |
||||
return `https://${site.domain}/image/${userProfile.picture[which]._id}`; |
return `https://${site.domain}/image/${userProfile.picture[which]._id}`; |
||||
} |
} |
||||
|
|
||||
|
function getUserFlags (user) { |
||||
|
const fA = `${user.flags.isAdmin ? 'A' : '-'}`; |
||||
|
const fM = `${user.flags.isModerator ? 'M' : '-'}`; |
||||
|
const fE = `${user.flags.isEmailVerified ? 'E' : '-'}`; |
||||
|
const fG = `${user.flags.isCloaked ? 'G' : '-'}`; |
||||
|
|
||||
|
const pL = `${user.permissions.canLogin ? 'L' : '-'}`; |
||||
|
const pC = `${user.permissions.canChat ? 'C' : '-'}`; |
||||
|
const pO = `${user.permissions.canComment ? 'O' : '-'}`; |
||||
|
const pR = `${user.permissions.canReport ? 'R' : '-'}`; |
||||
|
const pI = `${user.permissions.canShareLinks ? 'I' : '-'}`; |
||||
|
|
||||
|
return `${fA}${fM}${fE}${fG} ${pL}${pC}${pO}${pR}${pI}`; |
||||
|
} |
@ -0,0 +1,70 @@ |
|||||
|
// chat-processor.js
|
||||
|
// Copyright (C) 2024 DTP Technologies, LLC
|
||||
|
// All Rights Reserved
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import 'dotenv/config'; |
||||
|
|
||||
|
import path, { dirname } from 'path'; |
||||
|
|
||||
|
import { SiteRuntime } from '../../lib/site-lib.js'; |
||||
|
|
||||
|
import { CronJob } from 'cron'; |
||||
|
const CRON_TIMEZONE = 'America/New_York'; |
||||
|
|
||||
|
class ChatProcessorService extends SiteRuntime { |
||||
|
|
||||
|
static get name ( ) { return 'ChatProcessorService'; } |
||||
|
static get slug ( ) { return 'chatProcessor'; } |
||||
|
|
||||
|
constructor (rootPath) { |
||||
|
super(ChatProcessorService, rootPath); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
await super.start(); |
||||
|
|
||||
|
const mongoose = await import('mongoose'); |
||||
|
this.ChatMessage = mongoose.model('ChatMessage'); |
||||
|
|
||||
|
/* |
||||
|
* Cron jobs |
||||
|
*/ |
||||
|
|
||||
|
const messageExpireSchedule = '0 0 * * * *'; // Every hour
|
||||
|
this.cronJob = new CronJob( |
||||
|
messageExpireSchedule, |
||||
|
this.expireChatMessages.bind(this), |
||||
|
null, |
||||
|
true, |
||||
|
CRON_TIMEZONE, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async shutdown ( ) { |
||||
|
this.log.alert('ChatLinksWorker shutting down'); |
||||
|
await super.shutdown(); |
||||
|
} |
||||
|
|
||||
|
async expireChatMessages ( ) { |
||||
|
const { chat: chatService } = this.services; |
||||
|
await chatService.expireMessages(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
(async ( ) => { |
||||
|
|
||||
|
try { |
||||
|
const { fileURLToPath } = await import('node:url'); |
||||
|
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
||||
|
|
||||
|
const worker = new ChatProcessorService(path.resolve(__dirname, '..', '..')); |
||||
|
await worker.start(); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('failed to start chat processing worker', { error }); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
})(); |
@ -0,0 +1,84 @@ |
|||||
|
// worker-template.js
|
||||
|
// Copyright (C) 2024 DTP Technologies, LLC
|
||||
|
// All Rights Reserved
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import 'dotenv/config'; |
||||
|
|
||||
|
import path, { dirname } from 'path'; |
||||
|
|
||||
|
import { SiteRuntime } from '../../lib/site-lib.js'; |
||||
|
|
||||
|
import { CronJob } from 'cron'; |
||||
|
const CRON_TIMEZONE = 'America/New_York'; |
||||
|
|
||||
|
class TemplateService extends SiteRuntime { |
||||
|
|
||||
|
static get name ( ) { return 'TemplateService'; } |
||||
|
static get slug ( ) { return 'template'; } |
||||
|
|
||||
|
constructor (rootPath) { |
||||
|
super(TemplateService, rootPath); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
await super.start(); |
||||
|
|
||||
|
const mongoose = await import('mongoose'); |
||||
|
this.Link = mongoose.model('Link'); |
||||
|
|
||||
|
this.viewModel = { }; |
||||
|
await this.populateViewModel(this.viewModel); |
||||
|
|
||||
|
/* |
||||
|
* Cron jobs |
||||
|
*/ |
||||
|
|
||||
|
const cronJobSchedule = '*/5 * * * * *'; // Every 5 seconds
|
||||
|
this.cronJob = new CronJob( |
||||
|
cronJobSchedule, |
||||
|
this.cronJobProcessor.bind(this), |
||||
|
null, |
||||
|
true, |
||||
|
CRON_TIMEZONE, |
||||
|
); |
||||
|
|
||||
|
/* |
||||
|
* Bull Queue job processors |
||||
|
*/ |
||||
|
|
||||
|
this.log.info('registering queue job processor', { config: this.config.jobQueues.links }); |
||||
|
this.linksProcessingQueue = this.services.jobQueue.getJobQueue('links', this.config.jobQueues.links); |
||||
|
this.linksProcessingQueue.process('link-ingest', 1, this.ingestLink.bind(this)); |
||||
|
} |
||||
|
|
||||
|
async shutdown ( ) { |
||||
|
this.log.alert('ChatLinksWorker shutting down'); |
||||
|
await super.shutdown(); |
||||
|
} |
||||
|
|
||||
|
async cronJobProcessor ( ) { |
||||
|
this.log.info('your cron job is running now'); |
||||
|
} |
||||
|
|
||||
|
async ingestLink (job) { |
||||
|
this.log.info('processing queue job', { id: job.id }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
(async ( ) => { |
||||
|
|
||||
|
try { |
||||
|
const { fileURLToPath } = await import('node:url'); |
||||
|
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
||||
|
|
||||
|
const worker = new TemplateService(path.resolve(__dirname, '..', '..')); |
||||
|
await worker.start(); |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('failed to start template worker', { error }); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
})(); |
@ -1,9 +1,13 @@ |
|||||
@import "site/uk-lightbox.less"; |
@import "site/uk-lightbox.less"; |
||||
|
|
||||
@import "site/main.less"; |
@import "site/main.less"; |
||||
|
|
||||
@import "site/button.less"; |
@import "site/button.less"; |
||||
|
@import "site/drop-feedback.less"; |
||||
@import "site/emoji-picker.less"; |
@import "site/emoji-picker.less"; |
||||
@import "site/image.less"; |
@import "site/image.less"; |
||||
@import "site/link-preview.less"; |
@import "site/link-preview.less"; |
||||
|
@import "site/menu.less"; |
||||
@import "site/navbar.less"; |
@import "site/navbar.less"; |
||||
@import "site/stage.less"; |
@import "site/stage.less"; |
||||
|
@import "site/stats.less"; |
@ -0,0 +1,50 @@ |
|||||
|
.dtp-drop-feedback { |
||||
|
position: fixed; |
||||
|
top: 0; right: 0; bottom: 0; left: 0; |
||||
|
box-sizing: border-box; |
||||
|
|
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
|
||||
|
background-color: rgba(0,0,0, 0.8); |
||||
|
color: #e8e8e8; |
||||
|
|
||||
|
opacity: 0; |
||||
|
transition: opacity 0.2s; |
||||
|
|
||||
|
pointer-events: none; |
||||
|
|
||||
|
&.feedback-active { |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
.drop-feedback-container { |
||||
|
box-sizing: border-box; |
||||
|
padding: 20px; |
||||
|
|
||||
|
border: dotted 3px #e8e8e8; |
||||
|
border-radius: 10px; |
||||
|
|
||||
|
background-color: #48a8d4; |
||||
|
color: #e8e8e8; |
||||
|
|
||||
|
pointer-events: none; |
||||
|
|
||||
|
.feedback-icon { |
||||
|
margin-bottom: 10px; |
||||
|
|
||||
|
font-size: 2.5em; |
||||
|
line-height: 1; |
||||
|
|
||||
|
color: #ffffff; |
||||
|
|
||||
|
pointer-events: none; |
||||
|
} |
||||
|
|
||||
|
.drop-feedback-prompt { |
||||
|
font-size: 1.2em; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
.menu-icon { |
||||
|
width: 2em; |
||||
|
text-align: center; |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
.stats-block { |
||||
|
padding: 10px 20px; |
||||
|
|
||||
|
border: solid 1px red; |
||||
|
border-radius: 10px; |
||||
|
|
||||
|
background-color: #fff0f0; |
||||
|
color: #2a2a2a; |
||||
|
|
||||
|
.stat-label { |
||||
|
font-size: 0.8em; |
||||
|
} |
||||
|
|
||||
|
.stat-value { |
||||
|
font-size: 1.2em; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,157 @@ |
|||||
|
// dtp-chat-cli.js
|
||||
|
// Copyright (C) 2024 DTP Technologies, LLC
|
||||
|
// All Rights Reserved
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
import 'dotenv/config'; |
||||
|
|
||||
|
import { dirname } from 'path'; |
||||
|
import fs from 'fs'; |
||||
|
|
||||
|
import { fileURLToPath } from 'url'; |
||||
|
const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line
|
||||
|
|
||||
|
import mongoose from 'mongoose'; |
||||
|
|
||||
|
import { SiteRuntime } from './lib/site-runtime.js'; |
||||
|
|
||||
|
class SiteTerminalApp extends SiteRuntime { |
||||
|
|
||||
|
static get name ( ) { return 'SiteTerminalApp'; } |
||||
|
static get slug ( ) { return 'cliApp'; } |
||||
|
|
||||
|
constructor ( ) { |
||||
|
super(SiteTerminalApp, __dirname); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
await super.start(); |
||||
|
|
||||
|
this.processors = { |
||||
|
'grant': this.grant.bind(this), |
||||
|
'revoke': this.revoke.bind(this), |
||||
|
'probe': this.probeMediaFile.bind(this), |
||||
|
'transcodeMov': this.transcodeMov.bind(this), |
||||
|
'transcodeGif': this.transcodeGif.bind(this), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async shutdown ( ) { |
||||
|
await super.shutdown(); |
||||
|
return 0; // exitCode
|
||||
|
} |
||||
|
|
||||
|
async run (args) { |
||||
|
this.log.debug('running', { args }); |
||||
|
const command = args.shift(); |
||||
|
const processor = this.processors[command]; |
||||
|
if (!processor) { |
||||
|
this.log.error('Unknown command', { command }); |
||||
|
return; |
||||
|
} |
||||
|
return processor(args); |
||||
|
} |
||||
|
|
||||
|
async grant (args) { |
||||
|
const User = mongoose.model('User'); |
||||
|
|
||||
|
const privilege = args.shift(); |
||||
|
const username = args.shift(); |
||||
|
|
||||
|
const user = await User.findOne({ username_lc: username.toLowerCase().trim() }).select('_id'); |
||||
|
if (!user) { |
||||
|
throw new Error('User not found'); |
||||
|
} |
||||
|
|
||||
|
switch (privilege) { |
||||
|
case 'admin': |
||||
|
this.log.info('granting admin privileges'); |
||||
|
await User.updateOne({ _id: user._id }, { $set: { 'flags.isAdmin': true } }); |
||||
|
break; |
||||
|
case 'moderator': |
||||
|
this.log.info('granting moderator privileges'); |
||||
|
await User.updateOne({ _id: user._id }, { $set: { 'flags.isAdmin': true } }); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async revoke (args) { |
||||
|
const User = mongoose.model('User'); |
||||
|
|
||||
|
const username_lc = args.shift(); |
||||
|
const user = await User.findOne({ username_lc }).select('_id'); |
||||
|
if (!user) { |
||||
|
throw new Error('User not found'); |
||||
|
} |
||||
|
|
||||
|
const privilege = args.shift(); |
||||
|
switch (privilege) { |
||||
|
case 'admin': |
||||
|
this.log.info('revoking admin privileges'); |
||||
|
await User.updateOne({ _id: user._id }, { $set: { 'flags.isAdmin': false } }); |
||||
|
break; |
||||
|
case 'moderator': |
||||
|
this.log.info('revoking moderator privileges'); |
||||
|
await User.updateOne({ _id: user._id }, { $set: { 'flags.isAdmin': false } }); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async probeMediaFile (args) { |
||||
|
const { media: mediaService } = this.services; |
||||
|
const filename = args.shift(); |
||||
|
const probe = await mediaService.ffprobe(filename); |
||||
|
this.log.info('FFPROBE result', { probe }); |
||||
|
} |
||||
|
|
||||
|
async transcodeMov (args) { |
||||
|
const { video: videoService } = this.services; |
||||
|
|
||||
|
const filename = args.shift(); |
||||
|
const stat = await fs.promises.stat(filename); |
||||
|
const file = { |
||||
|
path: filename, |
||||
|
size: stat.size, |
||||
|
type: 'video/quicktime', |
||||
|
}; |
||||
|
|
||||
|
await videoService.transcodeMov(file); |
||||
|
|
||||
|
this.log.info('transcode output ready', { filename: file.path }); |
||||
|
} |
||||
|
|
||||
|
async transcodeGif (args) { |
||||
|
const { video: videoService } = this.services; |
||||
|
|
||||
|
const filename = args.shift(); |
||||
|
const stat = await fs.promises.stat(filename); |
||||
|
const file = { |
||||
|
path: filename, |
||||
|
size: stat.size, |
||||
|
type: 'image/gif', |
||||
|
}; |
||||
|
|
||||
|
await videoService.transcodeGif(file); |
||||
|
|
||||
|
this.log.info('transcode output ready', { filename: file.path }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
(async ( ) => { |
||||
|
|
||||
|
let app; |
||||
|
try { |
||||
|
app = new SiteTerminalApp(); |
||||
|
await app.start(); |
||||
|
await app.run(process.argv.slice(2)); |
||||
|
} catch (error) { |
||||
|
console.error('failed to start Chat terminal application', error); |
||||
|
} finally { |
||||
|
await app.shutdown(); |
||||
|
process.nextTick(( ) => { |
||||
|
process.exit(0); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
})(); |
Loading…
Reference in new issue