1
0
Fork 0
mirror of https://git.sr.ht/~cadence/cloudtube synced 2026-03-02 02:31:35 +00:00

Move utils folder and fix published text

This commit is contained in:
Cadence Ember 2020-09-24 00:05:02 +12:00
parent 643f1e0889
commit 2e69dfc4b7
No known key found for this signature in database
GPG key ID: BC1C2C61CF521B17
16 changed files with 53 additions and 619 deletions

25
utils/constants.js Normal file
View file

@ -0,0 +1,25 @@
const constants = {
user_settings: {
instance: {
type: "string",
default: "https://second.cadence.moe"
},
save_history: {
type: "boolean",
default: false
}
},
caching: {
csrf_time: 4*60*60*1000,
seen_token_subscriptions_eligible: 40*60*60*1000,
subscriptions_refresh_loop_min: 5*60*1000,
},
regex: {
ucid: "[A-Za-z0-9-_]+",
video_id: "[A-Za-z0-9-_]+"
}
}
module.exports = constants

21
utils/converters.js Normal file
View file

@ -0,0 +1,21 @@
function timeToPastText(timestamp) {
const difference = Date.now() - timestamp
return [
["year", 365 * 24 * 60 * 60 * 1000],
["month", 30 * 24 * 60 * 60 * 1000],
["week", 7 * 24 * 60 * 60 * 1000],
["day", 24 * 60 * 60 * 1000],
["hour", 60 * 60 * 1000],
["minute", 60 * 1000],
["second", 1 * 1000]
].reduce((acc, [unitName, unitValue]) => {
if (acc) return acc
if (difference > unitValue) {
const number = Math.floor(difference / unitValue)
const pluralUnit = unitName + (number == 1 ? "" : "s")
return `${number} ${pluralUnit} ago`
}
}, null) || "just now"
}
module.exports.timeToPastText = timeToPastText

8
utils/db.js Normal file
View file

@ -0,0 +1,8 @@
const sqlite = require("better-sqlite3")
const pj = require("path").join
const fs = require("fs")
const dir = pj(__dirname, "../db")
fs.mkdirSync(pj(dir, "backups"), {recursive: true})
const db = new sqlite(pj(dir, "cloudtube.db"))
module.exports = db

96
utils/getuser.js Normal file
View file

@ -0,0 +1,96 @@
const crypto = require("crypto")
const {parse: parseCookie} = require("cookie")
const constants = require("./constants")
const db = require("./db")
function getToken(req, responseHeaders) {
if (!req.headers.cookie) req.headers.cookie = ""
const cookie = parseCookie(req.headers.cookie)
let token = cookie.token
if (!token) {
if (responseHeaders) { // we should create a token
const setCookie = responseHeaders["set-cookie"] || []
token = crypto.randomBytes(18).toString("base64").replace(/\W/g, "_")
setCookie.push(`token=${token}; Path=/; Max-Age=2147483648; HttpOnly; SameSite=Lax`)
responseHeaders["set-cookie"] = setCookie
} else {
return null
}
}
db.prepare("REPLACE INTO SeenTokens (token, seen) VALUES (?, ?)").run([token, Date.now()])
return token
}
class User {
constructor(token) {
this.token = token
}
getSettings() {
if (this.token) {
return db.prepare("SELECT * FROM Settings WHERE token = ?").get(this.token) || {}
} else {
return {}
}
}
getSettingsOrDefaults() {
const settings = this.getSettings()
for (const key of Object.keys(constants.user_settings)) {
if (settings[key] == null) settings[key] = constants.user_settings[key].default
}
return settings
}
getSubscriptions() {
if (this.token) {
return db.prepare("SELECT ucid FROM Subscriptions WHERE token = ?").pluck().all(this.token)
} else {
return []
}
}
isSubscribed(ucid) {
if (this.token) {
return !!db.prepare("SELECT * FROM Subscriptions WHERE token = ? AND ucid = ?").get([this.token, ucid])
} else {
return false
}
}
}
/**
* @param {any} responseHeaders supply this to create a token
*/
function getUser(req, responseHeaders) {
const token = getToken(req, responseHeaders)
return new User(token)
}
function generateCSRF() {
const token = crypto.randomBytes(16).toString("hex")
const expires = Date.now() + constants.caching.csrf_time
db.prepare("INSERT INTO CSRFTokens (token, expires) VALUES (?, ?)").run(token, expires)
return token
}
function checkCSRF(token) {
const row = db.prepare("SELECT * FROM CSRFTokens WHERE token = ? AND expires > ?").get(token, Date.now())
if (row) {
db.prepare("DELETE FROM CSRFTokens WHERE token = ?").run(token)
return true
} else {
return false
}
}
function cleanCSRF() {
db.prepare("DELETE FROM CSRFTokens WHERE expires <= ?").run(Date.now())
}
cleanCSRF()
setInterval(cleanCSRF, constants.caching.csrf_time).unref()
module.exports.getToken = getToken
module.exports.generateCSRF = generateCSRF
module.exports.checkCSRF = checkCSRF
module.exports.getUser = getUser

61
utils/upgradedb.js Normal file
View file

@ -0,0 +1,61 @@
const pj = require("path").join
const db = require("./db")
const deltas = [
// 0: from empty file, +DatabaseVersion, +Subscriptions
function() {
db.prepare("CREATE TABLE DatabaseVersion (version INTEGER NOT NULL, PRIMARY KEY (version))")
.run()
db.prepare("CREATE TABLE Subscriptions (token TEXT NOT NULL, ucid TEXT NOT NULL, PRIMARY KEY (token, ucid))")
.run()
db.prepare("CREATE TABLE Channels (ucid TEXT NOT NULL, name TEXT NOT NULL, icon_url TEXT, PRIMARY KEY (ucid))")
.run()
db.prepare("CREATE TABLE Videos (videoId TEXT NOT NULL, title TEXT NOT NULL, author TEXT, authorId TEXT NOT NULL, published INTEGER, publishedText TEXT, lengthText TEXT, viewCountText TEXT, descriptionHtml TEXT, PRIMARY KEY (videoId))")
.run()
db.prepare("CREATE TABLE CSRFTokens (token TEXT NOT NULL, expires INTEGER NOT NULL, PRIMARY KEY (token))")
.run()
db.prepare("CREATE TABLE SeenTokens (token TEXT NOT NULL, seen INTEGER NOT NULL, PRIMARY KEY (token))")
.run()
db.prepare("CREATE TABLE Settings (token TEXT NOT NULL, instance TEXT, save_history INTEGER, PRIMARY KEY (token))")
.run()
}
]
async function createBackup(entry) {
const filename = `db/backups/cloudtube.db.bak-v${entry-1}`
process.stdout.write(`Backing up current to ${filename}... `)
await db.backup(pj(__dirname, "../../", filename))
process.stdout.write("done.\n")
}
/**
* @param {number} entry
* @param {boolean} log
*/
function runDelta(entry, log) {
process.stdout.write(`Upgrading database to version ${entry}... `)
deltas[entry]()
db.prepare("DELETE FROM DatabaseVersion").run()
db.prepare("INSERT INTO DatabaseVersion (version) VALUES (?)").run(entry)
process.stdout.write("done.\n")
}
module.exports = async function() {
let currentVersion = -1
const newVersion = deltas.length - 1
try {
currentVersion = db.prepare("SELECT version FROM DatabaseVersion").pluck().get()
} catch (e) {} // if the table doesn't exist yet then we don't care
if (currentVersion !== newVersion) {
// go through the entire upgrade sequence
for (let entry = currentVersion+1; entry <= newVersion; entry++) {
// Back up current version
if (entry > 0) await createBackup(entry)
// Run delta
runDelta(entry)
}
}
}

83
utils/validate.js Normal file
View file

@ -0,0 +1,83 @@
class V {
constructor() {
this.chain = []
this.state = {}
this.finished = false
this.endValue = null
}
with(preset) {
this.check(...preset)
return this
}
check(conditionCallback, elseCallback) {
this.chain.push(() => {
if (!conditionCallback(this.state)) this._end(elseCallback(this.state))
})
return this
}
last(callback) {
this.chain.push(() => {
this._end(callback(this.state))
})
return this
}
go() {
for (const s of this.chain) {
s()
if (this.finished) return this.endValue
}
return {
statusCode: 500,
contentType: "application/json",
content: {
error: "Reached end of V chain without response"
}
}
}
_end(value) {
this.finished = true
this.endValue = value
}
}
function presetLoad(additions) {
return [
state => {
Object.assign(state, additions)
return true
},
null
]
}
function presetURLParamsBody() {
return [
state => {
try {
state.params = new URLSearchParams(state.body.toString())
return true
} catch (e) {
console.error(e)
return false
}
},
() => {
return {
statusCode: 400,
contentType: "application/json",
content: {
error: "Could not parse body as URLSearchParams"
}
}
}
]
}
module.exports.V = V
module.exports.presetLoad = presetLoad
module.exports.presetURLParamsBody = presetURLParamsBody

2048
utils/words.txt Normal file

File diff suppressed because it is too large Load diff

16
utils/youtube.js Normal file
View file

@ -0,0 +1,16 @@
const fetch = require("node-fetch")
const db = require("./db")
async function fetchChannel(ucid, instance) {
if (!instance) throw new Error("No instance parameter provided")
// fetch
const channel = await fetch(`${instance}/api/v1/channels/${ucid}`).then(res => res.json())
// update database
const bestIcon = channel.authorThumbnails.slice(-1)[0]
const iconURL = bestIcon ? bestIcon.url : null
db.prepare("REPLACE INTO Channels (ucid, name, icon_url) VALUES (?, ?, ?)").run([channel.authorId, channel.author, iconURL])
// return
return channel
}
module.exports.fetchChannel = fetchChannel