mirror of
https://git.sr.ht/~cadence/bibliogram
synced 2024-11-26 09:37:28 +00:00
Add experimental assistant feature
This commit is contained in:
parent
160fa7d843
commit
b22028aaa4
@ -6,6 +6,7 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cd src/site && node server.js",
|
"start": "cd src/site && node server.js",
|
||||||
|
"assistant": "cd src/site && node assistant.js",
|
||||||
"test": "tap"
|
"test": "tap"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
@ -14,6 +14,9 @@ const userRequestCache = new UserRequestCache(constants.caching.resource_cache_t
|
|||||||
const timelineEntryCache = new TtlCache(constants.caching.resource_cache_time)
|
const timelineEntryCache = new TtlCache(constants.caching.resource_cache_time)
|
||||||
const history = new RequestHistory(["user", "timeline", "post", "reel"])
|
const history = new RequestHistory(["user", "timeline", "post", "reel"])
|
||||||
|
|
||||||
|
const AssistantSwitcher = require("./structures/AssistantSwitcher")
|
||||||
|
const assistantSwitcher = new AssistantSwitcher()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} username
|
* @param {string} username
|
||||||
* @param {boolean} isRSS
|
* @param {boolean} isRSS
|
||||||
@ -53,6 +56,11 @@ async function fetchUser(username, isRSS) {
|
|||||||
return fetchUserFromCombined(saved.user_id, username)
|
return fetchUserFromCombined(saved.user_id, username)
|
||||||
} else if (saved && saved.updated_version >= 2) {
|
} else if (saved && saved.updated_version >= 2) {
|
||||||
return fetchUserFromSaved(saved)
|
return fetchUserFromSaved(saved)
|
||||||
|
} else if (assistantSwitcher.enabled()) {
|
||||||
|
return assistantSwitcher.requestUser(username).catch(error => {
|
||||||
|
if (error === constants.symbols.NO_ASSISTANTS_AVAILABLE) throw constants.symbols.RATE_LIMITED
|
||||||
|
else throw error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
@ -332,3 +340,5 @@ module.exports.timelineEntryCache = timelineEntryCache
|
|||||||
module.exports.getOrFetchShortcode = getOrFetchShortcode
|
module.exports.getOrFetchShortcode = getOrFetchShortcode
|
||||||
module.exports.updateProfilePictureFromReel = updateProfilePictureFromReel
|
module.exports.updateProfilePictureFromReel = updateProfilePictureFromReel
|
||||||
module.exports.history = history
|
module.exports.history = history
|
||||||
|
module.exports.fetchUserFromSaved = fetchUserFromSaved
|
||||||
|
module.exports.assistantSwitcher = assistantSwitcher
|
||||||
|
@ -40,6 +40,15 @@ let constants = {
|
|||||||
enable_updater_page: false
|
enable_updater_page: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
assistant: {
|
||||||
|
enabled: false,
|
||||||
|
// List of assistant origin URLs, if you have any.
|
||||||
|
origins: [
|
||||||
|
],
|
||||||
|
offline_request_cooldown: 20*60*1000,
|
||||||
|
blocked_request_cooldown: 2*60*60*1000,
|
||||||
|
},
|
||||||
|
|
||||||
caching: {
|
caching: {
|
||||||
image_cache_control: `public, max-age=${7*24*60*60}`,
|
image_cache_control: `public, max-age=${7*24*60*60}`,
|
||||||
resource_cache_time: 30*60*1000,
|
resource_cache_time: 30*60*1000,
|
||||||
@ -78,7 +87,14 @@ let constants = {
|
|||||||
NO_SHARED_DATA: Symbol("NO_SHARED_DATA"),
|
NO_SHARED_DATA: Symbol("NO_SHARED_DATA"),
|
||||||
INSTAGRAM_DEMANDS_LOGIN: Symbol("INSTAGRAM_DEMANDS_LOGIN"),
|
INSTAGRAM_DEMANDS_LOGIN: Symbol("INSTAGRAM_DEMANDS_LOGIN"),
|
||||||
RATE_LIMITED: Symbol("RATE_LIMITED"),
|
RATE_LIMITED: Symbol("RATE_LIMITED"),
|
||||||
ENDPOINT_OVERRIDDEN: Symbol("ENDPOINT_OVERRIDDEN")
|
ENDPOINT_OVERRIDDEN: Symbol("ENDPOINT_OVERRIDDEN"),
|
||||||
|
NO_ASSISTANTS_AVAILABLE: Symbol("NO_ASSISTANTS_AVAILABLE"),
|
||||||
|
assistant_statuses: {
|
||||||
|
OFFLINE: Symbol("OFFLINE"),
|
||||||
|
BLOCKED: Symbol("BLOCKED"),
|
||||||
|
OK: Symbol("OK"),
|
||||||
|
NONE: Symbol("NONE")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
database_version: 2
|
database_version: 2
|
||||||
|
42
src/lib/structures/Assistant.js
Normal file
42
src/lib/structures/Assistant.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
const {request} = require("../utils/request")
|
||||||
|
const constants = require("../constants")
|
||||||
|
|
||||||
|
class Assistant {
|
||||||
|
constructor(origin) {
|
||||||
|
this.origin = origin
|
||||||
|
this.lastRequest = 0
|
||||||
|
this.lastRequestStatus = constants.symbols.assistant_statuses.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
available() {
|
||||||
|
if (this.lastRequestStatus === constants.symbols.assistant_statuses.OFFLINE) {
|
||||||
|
return Date.now() - this.lastRequest > constants.assistant.offline_request_cooldown
|
||||||
|
} else if (this.lastRequestStatus === constants.symbols.assistant_statuses.BLOCKED) {
|
||||||
|
return Date.now() - this.lastRequest > constants.assistant.blocked_request_cooldown
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUser(username) {
|
||||||
|
this.lastRequest = Date.now()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request(`${this.origin}/api/user/v1/${username}`).json().then(root => {
|
||||||
|
console.log(root)
|
||||||
|
if (root.status === "ok") {
|
||||||
|
this.lastRequestStatus = constants.symbols.assistant_statuses.OK
|
||||||
|
resolve(root.data.user)
|
||||||
|
} else {
|
||||||
|
this.lastRequestStatus = constants.symbols.assistant_statuses.BLOCKED
|
||||||
|
reject(constants.symbols.assistant_statuses.BLOCKED)
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
this.lastRequestStatus = constants.symbols.assistant_statuses.OFFLINE
|
||||||
|
reject(constants.symbols.assistant_statuses.OFFLINE)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Assistant
|
49
src/lib/structures/AssistantSwitcher.js
Normal file
49
src/lib/structures/AssistantSwitcher.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
const constants = require("../constants")
|
||||||
|
const collectors = require("../collectors")
|
||||||
|
const Assistant = require("./Assistant")
|
||||||
|
const db = require("../db")
|
||||||
|
|
||||||
|
class AssistantSwitcher {
|
||||||
|
constructor() {
|
||||||
|
this.assistants = constants.assistant.origins.map(origin => new Assistant(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled() {
|
||||||
|
return constants.assistant.enabled && this.assistants.length
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableAssistants() {
|
||||||
|
return this.assistants.filter(assistant => assistant.available()).sort((a, b) => (a.lastRequest - b.lastRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
requestUser(username) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const assistants = this.getAvailableAssistants()
|
||||||
|
while (assistants.length) {
|
||||||
|
const assistant = assistants.shift()
|
||||||
|
try {
|
||||||
|
const user = await assistant.requestUser(username)
|
||||||
|
return resolve(user)
|
||||||
|
} catch (e) {
|
||||||
|
// that assistant broke. try the next one.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reject(constants.symbols.NO_ASSISTANTS_AVAILABLE)
|
||||||
|
}).then(user => {
|
||||||
|
const bind = {...user}
|
||||||
|
bind.created = Date.now()
|
||||||
|
bind.updated = Date.now()
|
||||||
|
bind.updated_version = constants.database_version
|
||||||
|
bind.is_private = +user.is_private
|
||||||
|
bind.is_verified = +user.is_verified
|
||||||
|
db.prepare(
|
||||||
|
"REPLACE INTO Users (username, user_id, created, updated, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url) VALUES "
|
||||||
|
+"(@username, @user_id, @created, @updated, @updated_version, @biography, @post_count, @following_count, @followed_by_count, @external_url, @full_name, @is_private, @is_verified, @profile_pic_url)"
|
||||||
|
).run(bind)
|
||||||
|
collectors.userRequestCache.cache.delete(`user/${username}`)
|
||||||
|
return collectors.fetchUserFromSaved(user)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = AssistantSwitcher
|
@ -78,7 +78,7 @@ module.exports = [
|
|||||||
message: "This user doesn't exist.",
|
message: "This user doesn't exist.",
|
||||||
withInstancesLink: false
|
withInstancesLink: false
|
||||||
})
|
})
|
||||||
} else if (error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN) {
|
} else if (error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN || error === constants.symbols.RATE_LIMITED) {
|
||||||
return {
|
return {
|
||||||
statusCode: 503,
|
statusCode: 503,
|
||||||
contentType: "text/html",
|
contentType: "text/html",
|
||||||
|
40
src/site/assistant.js
Normal file
40
src/site/assistant.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const {Pinski} = require("pinski")
|
||||||
|
const {subdirs} = require("node-dir")
|
||||||
|
const constants = require("../lib/constants")
|
||||||
|
|
||||||
|
const passthrough = require("./passthrough")
|
||||||
|
|
||||||
|
const pinski = new Pinski({
|
||||||
|
port: constants.port,
|
||||||
|
relativeRoot: __dirname
|
||||||
|
})
|
||||||
|
|
||||||
|
;(async (err, dirs) => {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
// need to check for and run db upgrades before anything starts using it
|
||||||
|
await require("../lib/utils/upgradedb")()
|
||||||
|
|
||||||
|
if (constants.tor.enabled) {
|
||||||
|
await require("../lib/utils/tor") // make sure tor state is known before going further
|
||||||
|
}
|
||||||
|
|
||||||
|
pinski.addAPIDir("assistant_api")
|
||||||
|
pinski.startServer()
|
||||||
|
pinski.enableWS()
|
||||||
|
|
||||||
|
require("pinski/plugins").setInstance(pinski)
|
||||||
|
|
||||||
|
Object.assign(passthrough, pinski.getExports())
|
||||||
|
|
||||||
|
console.log("Assistant started")
|
||||||
|
|
||||||
|
if (constants.allow_user_from_reel !== "never") {
|
||||||
|
constants.allow_user_from_reel = "never"
|
||||||
|
console.log(`[!] You are running the assistant, so \`constants.allow_user_from_reel\` has been set to "never" for this session.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.stdin.isTTY || process.argv.includes("--enable-repl")) {
|
||||||
|
require("./repl")
|
||||||
|
}
|
||||||
|
})()
|
75
src/site/assistant_api/user.js
Normal file
75
src/site/assistant_api/user.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const constants = require("../../lib/constants")
|
||||||
|
const collectors = require("../../lib/collectors")
|
||||||
|
const db = require("../../lib/db")
|
||||||
|
|
||||||
|
function reply(statusCode, content) {
|
||||||
|
return {
|
||||||
|
statusCode: statusCode,
|
||||||
|
contentType: "application/json",
|
||||||
|
content: JSON.stringify(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
route: `/api/user`, methods: ["GET"], code: () => {
|
||||||
|
return Promise.resolve(reply(200, {
|
||||||
|
status: "ok",
|
||||||
|
generatedAt: Date.now(),
|
||||||
|
availableVersions: ["1"]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
route: `/api/user/v1/(${constants.external.username_regex})`, methods: ["GET"], code: async ({fill, url}) => {
|
||||||
|
function replyWithUserData(userData, type) {
|
||||||
|
return reply(200, {
|
||||||
|
status: "ok",
|
||||||
|
version: "1.0",
|
||||||
|
generatedAt: Date.now(),
|
||||||
|
data: {
|
||||||
|
type,
|
||||||
|
allow_user_from_reel: constants.allow_user_from_reel,
|
||||||
|
user: userData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = fill[0]
|
||||||
|
const saved = db.prepare("SELECT username, user_id, updated_version, biography, post_count, following_count, followed_by_count, external_url, full_name, is_private, is_verified, profile_pic_url FROM Users WHERE username = ?").get(username)
|
||||||
|
if (saved && saved.updated_version >= 2) { // suitable data is already saved
|
||||||
|
delete saved.updated_version
|
||||||
|
return Promise.resolve(replyWithUserData(saved, "user"))
|
||||||
|
} else {
|
||||||
|
return collectors.fetchUser(username, false).then(user => {
|
||||||
|
const type = user.constructor.name === "User" ? "user" : "reel"
|
||||||
|
return replyWithUserData({
|
||||||
|
username: user.data.username,
|
||||||
|
user_id: user.data.id,
|
||||||
|
biography: user.data.biography,
|
||||||
|
post_count: user.posts,
|
||||||
|
following_count: user.following,
|
||||||
|
followed_by_count: user.followedBy,
|
||||||
|
external_url: user.data.external_url,
|
||||||
|
full_name: user.data.full_name,
|
||||||
|
is_private: user.data.is_private,
|
||||||
|
is_verified: user.data.is_verified,
|
||||||
|
profile_pic_url: user.data.profile_pic_url
|
||||||
|
}, type)
|
||||||
|
}).catch(error => {
|
||||||
|
if (error === constants.symbols.RATE_LIMITED || error === constants.symbols.INSTAGRAM_DEMANDS_LOGIN) {
|
||||||
|
return reply(503, {
|
||||||
|
status: "fail",
|
||||||
|
version: "1.0",
|
||||||
|
generatedAt: Date.now(),
|
||||||
|
message: "Rate limited by Instagram.",
|
||||||
|
identifier: "RATE_LIMITED"
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -1,6 +1,7 @@
|
|||||||
const {instance, pugCache, wss} = require("./passthrough")
|
const {instance, pugCache, wss} = require("./passthrough")
|
||||||
const {userRequestCache, timelineEntryCache, history} = require("../lib/collectors")
|
const {userRequestCache, timelineEntryCache, history} = require("../lib/collectors")
|
||||||
const constants = require("../lib/constants")
|
const constants = require("../lib/constants")
|
||||||
|
const collectors = require("../lib/collectors")
|
||||||
const util = require("util")
|
const util = require("util")
|
||||||
const repl = require("repl")
|
const repl = require("repl")
|
||||||
const vm = require("vm")
|
const vm = require("vm")
|
||||||
|
@ -34,7 +34,6 @@ subdirs("pug", async (err, dirs) => {
|
|||||||
|
|
||||||
pinski.addAPIDir("api")
|
pinski.addAPIDir("api")
|
||||||
pinski.startServer()
|
pinski.startServer()
|
||||||
pinski.enableWS()
|
|
||||||
|
|
||||||
require("pinski/plugins").setInstance(pinski)
|
require("pinski/plugins").setInstance(pinski)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user