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:
parent
643f1e0889
commit
2e69dfc4b7
16 changed files with 53 additions and 619 deletions
25
utils/constants.js
Normal file
25
utils/constants.js
Normal 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
21
utils/converters.js
Normal 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
8
utils/db.js
Normal 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
96
utils/getuser.js
Normal 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
61
utils/upgradedb.js
Normal 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
83
utils/validate.js
Normal 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
2048
utils/words.txt
Normal file
File diff suppressed because it is too large
Load diff
16
utils/youtube.js
Normal file
16
utils/youtube.js
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue