This commit is contained in:
Nathan Rashleigh committed 2024-04-11 16:39:51 +10:00
1 parent 763b225fc2
commit 805809fa93
9 files changed
+175 -9

No files matched your search

-1
View File
@@ -60,7 +60,6 @@ async function processChannels(channelIds) {
return results return results
} }
async function updateMeili() { async function updateMeili() {
await ensureMeiliIndex(); await ensureMeiliIndex();
const index = meili.index('memes') const index = meili.index('memes')
+5 -1
View File
@@ -10,5 +10,9 @@
"name": "Memestream - npm start", "name": "Memestream - npm start",
"cmd": ["npm", "start"], "cmd": ["npm", "start"],
}, },
], {
"name": "Memestream - npm test",
"cmd": ["npm", "test"],
}
]
} }
+14
View File
@@ -13,6 +13,7 @@
"@prisma/client": "^5.12.1", "@prisma/client": "^5.12.1",
"axios": "^1.6.8", "axios": "^1.6.8",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"lodash": "^4.17.21",
"meilisearch": "^0.38.0", "meilisearch": "^0.38.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"minio": "^7.1.3", "minio": "^7.1.3",
@@ -22,6 +23,7 @@
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.0",
"esrun": "^3.2.26", "esrun": "^3.2.26",
"prisma": "^5.12.1" "prisma": "^5.12.1"
} }
@@ -1012,6 +1014,12 @@
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="
}, },
"node_modules/@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"node_modules/@types/long": { "node_modules/@types/long": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
@@ -3462,6 +3470,12 @@
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="
}, },
"@types/lodash": {
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"@types/long": { "@types/long": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+4 -2
View File
@@ -6,9 +6,9 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "esrun index.ts | pino-pretty", "start": "esrun index.ts | pino-pretty",
"dev": "esrun --watch index.ts | | pino-pretty", "dev": "esrun --watch index.ts | pino-pretty",
"migrate:make": "prisma migrate dev --name", "migrate:make": "prisma migrate dev --name",
"test": "echo \"Error: no test specified\" && exit 1" "test": "esrun src/scraper.test.ts | pino-pretty"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@@ -17,6 +17,7 @@
"@prisma/client": "^5.12.1", "@prisma/client": "^5.12.1",
"axios": "^1.6.8", "axios": "^1.6.8",
"dotenv": "^16.4.1", "dotenv": "^16.4.1",
"lodash": "^4.17.21",
"meilisearch": "^0.38.0", "meilisearch": "^0.38.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"minio": "^7.1.3", "minio": "^7.1.3",
@@ -26,6 +27,7 @@
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.0",
"esrun": "^3.2.26", "esrun": "^3.2.26",
"prisma": "^5.12.1" "prisma": "^5.12.1"
} }
@@ -0,0 +1,9 @@
-- CreateTable
CREATE TABLE "channelhistory" (
"id" BIGINT NOT NULL,
"earliestMessageId" BIGINT NOT NULL,
"latestMessageId" BIGINT NOT NULL,
"backfillComplete" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "channelhistory_pkey" PRIMARY KEY ("id")
);
@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `backfillComplete` on the `channelhistory` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "channelhistory" DROP COLUMN "backfillComplete";
+7
View File
@@ -49,4 +49,11 @@ model Meme {
@@map("memes") @@map("memes")
} }
model ChannelHistory {
id BigInt @id
earliestMessageId BigInt
latestMessageId BigInt
// we do this because you have to always wrap table names in "" if they have capitals
@@map("channelhistory")
}
+44
View File
@@ -0,0 +1,44 @@
import "dotenv/config"
import fs from 'fs/promises'
import pino from 'pino'
import * as Minio from 'minio'
import Vision from '@google-cloud/vision'
import { PrismaClient } from '@prisma/client'
import { MeiliSearch } from 'meilisearch'
import discordScraper from './scraper'
import attachmentService from "./attachments"
// @ts-ignore - ts don't know wtf it talking about
BigInt.prototype.toJSON = function bigIntStringify() {
return this.toString();
};
const VISION_CREDENTIALS = (await import(process.env.GOOGLE_APPLICATION_CREDENTIALS, { assert: { type: "json" } })).default
const STORAGE_BUCKET = process.env.STORAGE_BUCKET
const log = pino({ base: {} });
const vision = new Vision.ImageAnnotatorClient({ credentials: VISION_CREDENTIALS })
const prisma = new PrismaClient()
const minio = new Minio.Client({
endPoint: process.env.STORAGE_URL,
accessKey: process.env.STORAGE_ACCESS_KEY,
secretKey: process.env.STORAGE_SECRET_KEY,
})
const meili = new MeiliSearch({
host: process.env.MEILI_URL,
apiKey: process.env.MEILI_MASTER_KEY,
})
const attachments = attachmentService(log, minio, prisma)
const scraper = discordScraper(process.env.DISCORD_AUTH, vision, minio, prisma, log)
const CHANNEL_IDS = [
367936426962190336n, // TPM shitposts
1095060717117374687n, // aella memes
]
await Promise.all(CHANNEL_IDS.map(id => scraper.getChannelHistory(id)))
+84 -5
View File
@@ -1,5 +1,7 @@
import axios from 'axios' import axios from 'axios'
import minBy from 'lodash/minBy'
import type { PrismaClient, Attachment } from '@prisma/client'; import type { PrismaClient, Attachment } from '@prisma/client';
import type { ImageAnnotatorClient } from '@google-cloud/vision'; import type { ImageAnnotatorClient } from '@google-cloud/vision';
import type { Logger } from 'pino'; import type { Logger } from 'pino';
@@ -20,6 +22,18 @@ export default function scraper(discordAuth, vision: ImageAnnotatorClient, minio
return res.data return res.data
} }
async function fetchChannelMessagesAfter(channelId, lastMessageId) {
const url = `https://discord.com/api/v9/channels/${channelId}/messages?after=${lastMessageId}`
const res = await axios.get(url, { headers })
return res.data
}
async function fetchChannelMessagesBefore(channelId, earliestMessageId) {
const url = `https://discord.com/api/v9/channels/${channelId}/messages?before=${earliestMessageId}`
const res = await axios.get(url, { headers })
return res.data
}
async function fetchGuildDetails(guildId) { async function fetchGuildDetails(guildId) {
const url = `https://discord.com/api/v9/guilds/${guildId}` const url = `https://discord.com/api/v9/guilds/${guildId}`
const res = await axios.get(url, { headers }) const res = await axios.get(url, { headers })
@@ -64,12 +78,12 @@ export default function scraper(discordAuth, vision: ImageAnnotatorClient, minio
.flat() .flat()
.filter(([a, m]) => acceptedContentTypes.includes(a.content_type)) .filter(([a, m]) => acceptedContentTypes.includes(a.content_type))
const attachmentIds = validAttachments.map(([a, m]) => a.id) // const attachmentIds = validAttachments.map(([a, m]) => a.id)
const alreadyProcessedIds = (await prisma.attachment // const alreadyProcessedIds = (await prisma.attachment
.findMany({ select: { id: true }, where: { id: { in: attachmentIds }}}) // .findMany({ select: { id: true }, where: { id: { in: attachmentIds }}})
).map(r => r.id.toString()) // attachment not a true attachment yet, id still string // ).map(r => r.id.toString()) // attachment not a true attachment yet, id still string
const newAttachments = validAttachments.filter(([a, m]) => !alreadyProcessedIds.includes(a.id)) const newAttachments = validAttachments.filter(([a, m]) => ![].includes(a.id))
log.info({ newAttachments: newAttachments.length, validAttachments: validAttachments.length }) log.info({ newAttachments: newAttachments.length, validAttachments: validAttachments.length })
@@ -116,7 +130,72 @@ export default function scraper(discordAuth, vision: ImageAnnotatorClient, minio
} }
} }
async function getChannelHistory(channelId) {
const channel = await fetchChannel(channelId)
const guild = await fetchGuildDetails(channel.guild_id)
const log = baseLog.child({ guild: guild.name, channel: channel.name, channelId })
const ensureHistory = async () => {
const stored = await prisma.channelHistory.findFirst({ where: { id: channelId }})
if (stored) return stored
const lastMessageId = channel.last_message_id
return await prisma.channelHistory.create({ data:
{ latestMessageId: lastMessageId, earliestMessageId: lastMessageId, id: channelId }
})
}
async function fetchNew() {
const history = await ensureHistory()
const messages = await fetchChannelMessagesAfter(channelId, history.latestMessageId)
if (messages.length === 0) return
const result = {
...history,
messages: messages.length,
}
const newLatestMessageId = messages.map(m => m.id)
log.info(newLatestMessageId)
}
async function fetchOld() {
const history = await ensureHistory()
const messages = await fetchChannelMessagesBefore(channelId, history.earliestMessageId)
if (messages.length === 0) return
const result = {
...history,
messages: messages.length,
}
const earliestMessageId = minBy(messages, 'id').id
const attachments = await filterAttachments(log, messages, guild, channel);
log.info({
earliestMessageId,
messages: messages.length,
attachments: attachments.length,
})
}
await fetchNew()
await fetchOld()
// process them
// const messages = await fetchChannelMessagesBefore(channelId, history.earliestMessageId);
// const result = {
// ...history,
// newMessages: newMessages.length,
// messages: messages.length,
// }
// get messages before earliestMessageId until there are 0
}
return { return {
getChannelAttachments, getChannelAttachments,
getChannelHistory,
} }
} }