Properly applied code quality tools

main
Štěpán Škorpil 2022-09-18 13:32:25 +02:00
rodzic 405cf2f181
commit 29acce3906
88 zmienionych plików z 4426 dodań i 13190 usunięć

Wyświetl plik

@ -12,9 +12,9 @@ ENV ELASTIC_URL='http://elastic:9200' \
FROM prebuild AS build FROM prebuild AS build
WORKDIR /srv WORKDIR /srv
COPY application/package*.json ./ COPY application/package*.json ./
RUN npm install RUN yarn
COPY application/. . COPY application/. .
RUN npm run build RUN yarn build
FROM build AS dev FROM build AS dev
CMD npx tsc --watch CMD npx tsc --watch

Wyświetl plik

@ -1,24 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:react/recommended",
"standard"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
}
}

Wyświetl plik

@ -1,4 +0,0 @@
{
"printWidth": 100,
"singleQuote": true
}

12635
application/package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -8,46 +8,43 @@
"url": "skorpil.cz", "url": "skorpil.cz",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "npx tsc --watch", "dev": "tsc --watch",
"clean": "npx rimraf dist", "clean": "rimraf dist",
"build": "npx tsc", "build": "tsc",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "node dist/app", "start": "node dist/app",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{src,test}/**/*.{ts,js}\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"start:deploy": "npm run start" "start:deploy": "yarn start"
}, },
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "^8.2.1", "@elastic/elasticsearch": "^8.4.0",
"axios": "^0.21.1", "axios": "^0.27.2",
"geoip-lite": "^1.4.6", "geoip-lite": "^1.4.6",
"npmlog": "^6.0.0", "npmlog": "^6.0.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"striptags": "^3.2.0", "striptags": "^3.2.0",
"typescript-collections": "^1.3.3", "typescript-collections": "^1.3.3",
"zod": "^3.11.6" "zod": "^3.19.1"
}, },
"devDependencies": { "devDependencies": {
"@types/geoip-lite": "^1.4.1", "@types/geoip-lite": "^1.4.1",
"@types/jest": "^27.0.2", "@types/jest": "^29.0.3",
"@types/node": "^18.7.18", "@types/node": "^18.7.18",
"@types/npmlog": "^4.1.3", "@types/npmlog": "^4.1.4",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.0.0",
"eslint": "^7.32.0", "eslint": "^8.23.1",
"eslint-config-standard": "^16.0.3", "eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-import": "^2.25.3", "eslint-plugin-import": "^2.25.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-n": "^15.0.0",
"eslint-plugin-promise": "^5.1.1", "eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.27.1", "jest": "^29.0.3",
"jest": "^27.3.0", "ts-jest": "^29.0.1",
"standard": "*", "typescript": "^4.3.0"
"ts-jest": "^27.0.7",
"typescript": "^4.3.5"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [
@ -62,5 +59,23 @@
}, },
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node" "testEnvironment": "node"
} },
"eslintConfig": {
"env": {
"es2021": true
},
"extends": [
"standard-with-typescript"
],
"parserOptions": {
"project": [
"tsconfig.json"
]
}
},
"eslintIgnore": [
"dist",
"node_modules"
],
"prettier": "prettier-config-standard"
} }

Wyświetl plik

@ -1,5 +1,5 @@
export class NoSupportedLinkError extends Error { export class NoSupportedLinkError extends Error {
public constructor (domain:string) { public constructor (domain: string) {
super(`No supported link node info link for ${domain}`) super(`No supported link node info link for ${domain}`)
} }
} }

Wyświetl plik

@ -2,9 +2,13 @@ import { retrieveWellKnown } from './retrieveWellKnown'
import { retrieveNodeInfo, NodeInfo } from './retrieveNodeInfo' import { retrieveNodeInfo, NodeInfo } from './retrieveNodeInfo'
import { NoSupportedLinkError } from './NoSupportedLinkError' import { NoSupportedLinkError } from './NoSupportedLinkError'
export const retrieveDomainNodeInfo = async (domain:string):Promise<NodeInfo> => { export const retrieveDomainNodeInfo = async (
domain: string
): Promise<NodeInfo> => {
const wellKnown = await retrieveWellKnown(domain) const wellKnown = await retrieveWellKnown(domain)
const link = wellKnown.links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0') const link = wellKnown.links.find(
(link) => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0'
)
if (typeof link === 'undefined') { if (typeof link === 'undefined') {
throw new NoSupportedLinkError(domain) throw new NoSupportedLinkError(domain)
} }

Wyświetl plik

@ -9,24 +9,26 @@ const schema = z.object({
name: z.string(), name: z.string(),
version: z.string() version: z.string()
}), }),
protocols: z.array( protocols: z.array(z.string()),
z.string() usage: z.optional(
z.object({
users: z.optional(
z.object({
total: z.optional(z.number()),
activeMonth: z.optional(z.number()),
activeHalfyear: z.optional(z.number())
})
),
localPosts: z.optional(z.number())
})
), ),
usage: z.optional(z.object({
users: z.optional(z.object({
total: z.optional(z.number()),
activeMonth: z.optional(z.number()),
activeHalfyear: z.optional(z.number())
})),
localPosts: z.optional(z.number())
})),
openRegistrations: z.optional(z.boolean()) openRegistrations: z.optional(z.boolean())
}) })
export type NodeInfo = z.infer<typeof schema> export type NodeInfo = z.infer<typeof schema>
export const retrieveNodeInfo = async (url: string): Promise<NodeInfo> => { export const retrieveNodeInfo = async (url: string): Promise<NodeInfo> => {
console.info('Retrieving node info', { url: url }) console.info('Retrieving node info', { url })
const nodeInfoResponse = await axios.get(url, { const nodeInfoResponse = await axios.get(url, {
timeout: getDefaultTimeoutMilliseconds() timeout: getDefaultTimeoutMilliseconds()
}) })

Wyświetl plik

@ -15,7 +15,7 @@ const wellKnownSchema = z.object({
export type WellKnown = z.infer<typeof wellKnownSchema> export type WellKnown = z.infer<typeof wellKnownSchema>
export const retrieveWellKnown = async (domain: string): Promise<WellKnown> => { export const retrieveWellKnown = async (domain: string): Promise<WellKnown> => {
console.info('Retrieving well known', { domain: domain }) console.info('Retrieving well known', { domain })
const wellKnownUrl = `https://${domain}/.well-known/nodeinfo` const wellKnownUrl = `https://${domain}/.well-known/nodeinfo`
const wellKnownResponse = await axios.get(wellKnownUrl, { const wellKnownResponse = await axios.get(wellKnownUrl, {
timeout: getDefaultTimeoutMilliseconds(), timeout: getDefaultTimeoutMilliseconds(),

Wyświetl plik

@ -1,22 +1,22 @@
import { FieldData } from './FieldData' import { FieldData } from './FieldData'
export interface FeedData { export interface FeedData {
name:string, name: string
displayName:string, displayName: string
description:string, description: string
followersCount: number, followersCount: number
followingCount:number, followingCount: number
statusesCount?:number, statusesCount?: number
bot?:boolean, bot?: boolean
url: string, url: string
avatar?:string, avatar?: string
locked:boolean, locked: boolean
lastStatusAt?:Date, lastStatusAt?: Date
createdAt:Date createdAt: Date
fields: FieldData[], fields: FieldData[]
type: 'account'|'channel' type: 'account' | 'channel'
parentFeed?: { parentFeed?: {
name:string name: string
hostDomain:string hostDomain: string
} }
} }

Wyświetl plik

@ -1,6 +1,6 @@
import { FeedProviderMethod } from './FeedProviderMethod' import { FeedProviderMethod } from './FeedProviderMethod'
export interface FeedProvider { export interface FeedProvider {
getKey: ()=>string getKey: () => string
retrieveFeeds: FeedProviderMethod retrieveFeeds: FeedProviderMethod
} }

Wyświetl plik

@ -1,3 +1,6 @@
import { FeedData } from './FeedData' import { FeedData } from './FeedData'
export type FeedProviderMethod = (domain: string, page: number) => Promise<FeedData[]> export type FeedProviderMethod = (
domain: string,
page: number
) => Promise<FeedData[]>

Wyświetl plik

@ -1,5 +1,5 @@
export interface FieldData{ export interface FieldData {
name: string, name: string
value: string, value: string
verifiedAt: Date|undefined verifiedAt: Date | undefined
} }

Wyświetl plik

@ -6,14 +6,18 @@ import { FeedProvider } from '../FeedProvider'
const MastodonProvider: Provider = { const MastodonProvider: Provider = {
getKey: () => 'mastodon', getKey: () => 'mastodon',
getNodeProviders: ():NodeProvider[] => [{ getNodeProviders: (): NodeProvider[] => [
getKey: () => 'peers', {
retrieveNodes: retrievePeers getKey: () => 'peers',
}], retrieveNodes: retrievePeers
getFeedProviders: ():FeedProvider[] => [{ }
getKey: () => 'users', ],
retrieveFeeds: retrieveLocalPublicUsersPage getFeedProviders: (): FeedProvider[] => [
}] {
getKey: () => 'users',
retrieveFeeds: retrieveLocalPublicUsersPage
}
]
} }
export default MastodonProvider export default MastodonProvider

Wyświetl plik

@ -4,6 +4,7 @@ import { z } from 'zod'
import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMilliseconds' import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMilliseconds'
import { FeedProviderMethod } from '../FeedProviderMethod' import { FeedProviderMethod } from '../FeedProviderMethod'
import { NoMoreFeedsError } from '../NoMoreFeedsError' import { NoMoreFeedsError } from '../NoMoreFeedsError'
import { FeedData } from '../FeedData'
const limit = 500 const limit = 500
@ -41,19 +42,22 @@ const schema = z.array(
type Emoji = z.infer<typeof emojiSchema> type Emoji = z.infer<typeof emojiSchema>
const replaceEmojis = (text: string, emojis: Emoji[]): string => { const replaceEmojis = (text: string, emojis: Emoji[]): string => {
emojis.forEach(emoji => { emojis.forEach((emoji) => {
text = text.replace( text = text.replace(
RegExp(`:${emoji.shortcode}:`, 'gi'), RegExp(`:${emoji.shortcode}:`, 'gi'),
`<img draggable="false" class="emoji" title="${emoji.shortcode}" alt="${emoji.shortcode}" src="${emoji.url}" />` `<img draggable="false" class="emoji" title="${emoji.shortcode}" alt="${emoji.shortcode}" src="${emoji.url}" />`
) )
}) })
return text return text
} }
export const retrieveLocalPublicUsersPage: FeedProviderMethod = async (domain, page) => { export const retrieveLocalPublicUsersPage: FeedProviderMethod = async (
domain,
page
): Promise<FeedData[]> => {
const response = await axios.get('https://' + domain + '/api/v1/directory', { const response = await axios.get('https://' + domain + '/api/v1/directory', {
params: { params: {
limit: limit, limit,
offset: page * limit, offset: page * limit,
local: true local: true
}, },
@ -64,31 +68,33 @@ export const retrieveLocalPublicUsersPage: FeedProviderMethod = async (domain, p
if (responseData.length === 0) { if (responseData.length === 0) {
throw new NoMoreFeedsError('user') throw new NoMoreFeedsError('user')
} }
return responseData.map( return responseData.map((item) => {
item => { return {
return { name: item.username,
name: item.username, displayName: replaceEmojis(item.display_name, item.emojis),
displayName: replaceEmojis(item.display_name, item.emojis), description: replaceEmojis(item.note, item.emojis),
description: replaceEmojis(item.note, item.emojis), followersCount: item.followers_count,
followersCount: item.followers_count, followingCount: item.following_count,
followingCount: item.following_count, statusesCount: item.statuses_count,
statusesCount: item.statuses_count, bot: item.bot,
bot: item.bot, url: item.url,
url: item.url, avatar: item.avatar,
avatar: item.avatar, locked: item.locked,
locked: item.locked, lastStatusAt:
lastStatusAt: item.last_status_at !== null ? new Date(item.last_status_at) : null, item.last_status_at !== null
createdAt: new Date(item.created_at), ? new Date(item.last_status_at)
fields: item.fields.map(field => { : undefined,
return { createdAt: new Date(item.created_at),
name: replaceEmojis(field.name, item.emojis), fields: item.fields.map((field) => {
value: replaceEmojis(field.value, item.emojis), return {
verifiedAt: field.verified_at !== null ? new Date(field.verified_at) : null name: replaceEmojis(field.name, item.emojis),
} value: replaceEmojis(field.value, item.emojis),
}), verifiedAt:
type: 'account', field.verified_at !== null ? new Date(field.verified_at) : undefined
parentFeed: null }
} }),
type: 'account',
parentFeed: undefined
} }
) })
} }

Wyświetl plik

@ -5,17 +5,18 @@ import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMillisecon
import { NodeProviderMethod } from '../NodeProviderMethod' import { NodeProviderMethod } from '../NodeProviderMethod'
import { NoMoreNodesError } from '../NoMoreNodesError' import { NoMoreNodesError } from '../NoMoreNodesError'
const schema = z.array( const schema = z.array(z.string())
z.string()
)
export const retrievePeers:NodeProviderMethod = async (domain, page) => { export const retrievePeers: NodeProviderMethod = async (domain, page) => {
if (page !== 0) { if (page !== 0) {
throw new NoMoreNodesError('peer') throw new NoMoreNodesError('peer')
} }
const response = await axios.get('https://' + domain + '/api/v1/instance/peers', { const response = await axios.get(
timeout: getDefaultTimeoutMilliseconds() 'https://' + domain + '/api/v1/instance/peers',
}) {
timeout: getDefaultTimeoutMilliseconds()
}
)
assertSuccessJsonResponse(response) assertSuccessJsonResponse(response)
return schema.parse(response.data) return schema.parse(response.data)
} }

Wyświetl plik

@ -6,14 +6,18 @@ import { retrieveUsersPage } from './retrieveUsersPage'
const MisskeyProvider: Provider = { const MisskeyProvider: Provider = {
getKey: () => 'misskey', getKey: () => 'misskey',
getNodeProviders: ():NodeProvider[] => [{ getNodeProviders: (): NodeProvider[] => [
getKey: () => 'federation-instances', {
retrieveNodes: retrieveInstancesPage getKey: () => 'federation-instances',
}], retrieveNodes: retrieveInstancesPage
getFeedProviders: ():FeedProvider[] => [{ }
getKey: () => 'users', ],
retrieveFeeds: retrieveUsersPage getFeedProviders: (): FeedProvider[] => [
}] {
getKey: () => 'users',
retrieveFeeds: retrieveUsersPage
}
]
} }
export default MisskeyProvider export default MisskeyProvider

Wyświetl plik

@ -13,29 +13,34 @@ const schema = z.array(
}) })
) )
export const retrieveInstancesPage:NodeProviderMethod = async (domain, page) => { export const retrieveInstancesPage: NodeProviderMethod = async (
const response = await axios.post('https://' + domain + '/api/federation/instances', { domain,
host: null, page
blocked: null, ) => {
notResponding: null, const response = await axios.post(
suspended: null, 'https://' + domain + '/api/federation/instances',
federating: null, {
subscribing: null, host: null,
publishing: null, blocked: null,
limit: limit, notResponding: null,
offset: page * limit, suspended: null,
sort: '+id' federating: null,
}, { subscribing: null,
timeout: getDefaultTimeoutMilliseconds() publishing: null,
}) limit,
offset: page * limit,
sort: '+id'
},
{
timeout: getDefaultTimeoutMilliseconds()
}
)
assertSuccessJsonResponse(response) assertSuccessJsonResponse(response)
const responseData = schema.parse(response.data) const responseData = schema.parse(response.data)
if (responseData.length === 0) { if (responseData.length === 0) {
throw new NoMoreNodesError('instance') throw new NoMoreNodesError('instance')
} }
return responseData.map( return responseData.map((item) => {
item => { return item.host
return item.host })
}
)
} }

Wyświetl plik

@ -4,6 +4,8 @@ import { z } from 'zod'
import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMilliseconds' import { getDefaultTimeoutMilliseconds } from '../../getDefaultTimeoutMilliseconds'
import { NoMoreFeedsError } from '../NoMoreFeedsError' import { NoMoreFeedsError } from '../NoMoreFeedsError'
import { FeedProviderMethod } from '../FeedProviderMethod' import { FeedProviderMethod } from '../FeedProviderMethod'
import { FeedData } from '../FeedData'
import { FieldData } from '../FieldData'
const limit = 100 const limit = 100
@ -42,72 +44,84 @@ const schema = z.array(
type Emoji = z.infer<typeof emojiSchema> type Emoji = z.infer<typeof emojiSchema>
const replaceEmojis = (text: string, emojis: Emoji[]): string => { const replaceEmojis = (text: string, emojis: Emoji[]): string => {
emojis.forEach(emoji => { emojis.forEach((emoji) => {
text = text.replace( text = text.replace(
RegExp(`:${emoji.name}:`, 'gi'), RegExp(`:${emoji.name}:`, 'gi'),
`<img draggable="false" class="emoji" title="${emoji.name}" alt="${emoji.name}" src="${emoji.url}" />` `<img draggable="false" class="emoji" title="${emoji.name}" alt="${emoji.name}" src="${emoji.url}" />`
) )
}) })
return text return text
} }
const parseDescription = (description:string|null):string => { const parseDescription = (description: string | null): string => {
if (typeof description !== 'string') { if (typeof description !== 'string') {
return '' return ''
} }
return description.split('\n\n').map(paragraph => { return description
paragraph = paragraph.replace('\n', '</br>\n') .split('\n\n')
return `<p>${paragraph}</p>` .map((paragraph) => {
}).join('\n') paragraph = paragraph.replace('\n', '</br>\n')
return `<p>${paragraph}</p>`
})
.join('\n')
} }
export const retrieveUsersPage:FeedProviderMethod = async (domain, page) => { export const retrieveUsersPage: FeedProviderMethod = async (
const response = await axios.post('https://' + domain + '/api/users', { domain,
state: 'all', page
origin: 'local', ): Promise<FeedData[]> => {
sort: '+createdAt', const response = await axios.post(
limit: limit, 'https://' + domain + '/api/users',
offset: limit * page {
}, { state: 'all',
timeout: getDefaultTimeoutMilliseconds() origin: 'local',
}) sort: '+createdAt',
limit,
offset: limit * page
},
{
timeout: getDefaultTimeoutMilliseconds()
}
)
assertSuccessJsonResponse(response) assertSuccessJsonResponse(response)
const responseData = schema.parse(response.data) const responseData = schema.parse(response.data)
if (responseData.length === 0) { if (responseData.length === 0) {
throw new NoMoreFeedsError('user') throw new NoMoreFeedsError('user')
} }
return responseData.map( return responseData.map((item) => {
item => { return {
return { name: item.username,
name: item.username, displayName: replaceEmojis(item.name ?? item.username, item.emojis),
displayName: replaceEmojis(item.name ?? item.username, item.emojis), description: replaceEmojis(
description: replaceEmojis(parseDescription(item.description ?? ''), item.emojis), parseDescription(item.description ?? ''),
followersCount: item.followersCount, item.emojis
followingCount: item.followingCount, ),
statusesCount: item.notesCount, followersCount: item.followersCount,
bot: item.isBot, followingCount: item.followingCount,
url: `https://${domain}/@${item.username}`, statusesCount: item.notesCount,
avatar: item.avatarUrl, bot: item.isBot,
locked: item.isLocked, url: `https://${domain}/@${item.username}`,
lastStatusAt: item.updatedAt !== null ? new Date(item.updatedAt) : undefined, avatar: item.avatarUrl ?? undefined,
createdAt: new Date(item.createdAt), locked: item.isLocked,
fields: [ lastStatusAt:
...item.fields.map(field => { item.updatedAt !== null ? new Date(item.updatedAt) : undefined,
return { createdAt: new Date(item.createdAt),
name: replaceEmojis(field.name, item.emojis), fields: [
value: replaceEmojis(field.value, item.emojis), ...item.fields.map((field) => {
verifiedAt: undefined return {
} name: replaceEmojis(field.name, item.emojis),
}), value: replaceEmojis(field.value, item.emojis),
...[ verifiedAt: undefined
{ name: 'Location', value: item.location, verifiedAt: undefined }, }
{ name: 'Birthday', value: item.birthday, verifiedAt: undefined }, }),
{ name: 'Language', value: item.lang, verifiedAt: undefined } ...([
].filter(field => field.value !== null) { name: 'Location', value: item.location, verifiedAt: undefined },
], { name: 'Birthday', value: item.birthday, verifiedAt: undefined },
type: 'account', { name: 'Language', value: item.lang, verifiedAt: undefined }
parentFeed: undefined ].filter((field) => field.value !== null) as FieldData[])
} ],
type: 'account',
parentFeed: undefined
} }
) })
} }

Wyświetl plik

@ -1,5 +1,5 @@
export class NoMoreFeedsError extends Error { export class NoMoreFeedsError extends Error {
public constructor (feedType:string) { public constructor (feedType: string) {
super(`No more feeds of type ${feedType}`) super(`No more feeds of type ${feedType}`)
} }
} }

Wyświetl plik

@ -1,5 +1,5 @@
export class NoMoreNodesError extends Error { export class NoMoreNodesError extends Error {
public constructor (nodeType:string) { public constructor (nodeType: string) {
super(`No more nodes of type ${nodeType}`) super(`No more nodes of type ${nodeType}`)
} }
} }

Wyświetl plik

@ -1,6 +1,6 @@
import { NodeProviderMethod } from './NodeProviderMethod' import { NodeProviderMethod } from './NodeProviderMethod'
export interface NodeProvider { export interface NodeProvider {
getKey:()=>string, getKey: () => string
retrieveNodes: NodeProviderMethod retrieveNodes: NodeProviderMethod
} }

Wyświetl plik

@ -1,2 +1,4 @@
export type NodeProviderMethod = (
export type NodeProviderMethod = (domain: string, page:number)=> Promise<string[]> domain: string,
page: number
) => Promise<string[]>

Wyświetl plik

@ -1,7 +1,11 @@
import { z } from 'zod' import { z } from 'zod'
export const avatarSchema = z.optional(z.nullable(z.object({ export const avatarSchema = z.optional(
path: z.string() z.nullable(
}))) z.object({
path: z.string()
})
)
)
export type Avatar = z.infer<typeof avatarSchema> export type Avatar = z.infer<typeof avatarSchema>

Wyświetl plik

@ -7,16 +7,21 @@ import { retrieveFollowers } from './retrieveFollowers'
const PeertubeProvider: Provider = { const PeertubeProvider: Provider = {
getKey: () => 'peertube', getKey: () => 'peertube',
getNodeProviders: ():NodeProvider[] => [{ getNodeProviders: (): NodeProvider[] => [
getKey: () => 'followers', {
retrieveNodes: retrieveFollowers getKey: () => 'followers',
}], retrieveNodes: retrieveFollowers
getFeedProviders: ():FeedProvider[] => [{ }
getKey: () => 'accounts', ],
retrieveFeeds: retrieveAccounts getFeedProviders: (): FeedProvider[] => [
}, { {
getKey: () => 'video-channels', getKey: () => 'accounts',
retrieveFeeds: retrieveVideoChannels retrieveFeeds: retrieveAccounts
}] },
{
getKey: () => 'video-channels',
retrieveFeeds: retrieveVideoChannels
}
]
} }
export default PeertubeProvider export default PeertubeProvider

Wyświetl plik

@ -1,7 +1,10 @@
import { Avatar } from './Avatar' import { Avatar } from './Avatar'
export const parseAvatarUrl = (data:Avatar, domain:string):string|undefined => { export const parseAvatarUrl = (
if (data === null) { data: Avatar,
domain: string
): string | undefined => {
if (data === null || data === undefined) {
return undefined return undefined
} }
return `https://${domain}${data.path}` return `https://${domain}${data.path}`

Wyświetl plik

@ -1,9 +1,12 @@
export const parseDescription = (description:string|null):string => { export const parseDescription = (description: string | null): string => {
if (typeof description !== 'string') { if (typeof description !== 'string') {
return '' return ''
} }
return description.split('\n\n').map(paragraph => { return description
paragraph = paragraph.replace('\n', '</br>\n') .split('\n\n')
return `<p>${paragraph}</p>` .map((paragraph) => {
}).join('\n') paragraph = paragraph.replace('\n', '</br>\n')
return `<p>${paragraph}</p>`
})
.join('\n')
} }

Wyświetl plik

@ -29,7 +29,7 @@ const schema = z.object({
) )
}) })
export const retrieveAccounts:FeedProviderMethod = async (domain, page) => { export const retrieveAccounts: FeedProviderMethod = async (domain, page) => {
const response = await axios.get(`https://${domain}/api/v1/accounts`, { const response = await axios.get(`https://${domain}/api/v1/accounts`, {
params: { params: {
count: limit, count: limit,
@ -44,8 +44,8 @@ export const retrieveAccounts:FeedProviderMethod = async (domain, page) => {
throw new NoMoreFeedsError('account') throw new NoMoreFeedsError('account')
} }
return responseData.data return responseData.data
.filter(item => item.host === domain) .filter((item) => item.host === domain)
.map((item):FeedData => { .map((item): FeedData => {
return { return {
name: item.name, name: item.name,
url: item.url ?? `https://${domain}/accounts/${item.name}/`, url: item.url ?? `https://${domain}/accounts/${item.name}/`,

Wyświetl plik

@ -21,19 +21,22 @@ const schema = z.object({
) )
}) })
export const retrieveFollowers:NodeProviderMethod = async (domain, page) => { export const retrieveFollowers: NodeProviderMethod = async (domain, page) => {
const response = await axios.get(`https://${domain}/api/v1/server/followers`, { const response = await axios.get(
params: { `https://${domain}/api/v1/server/followers`,
count: limit, {
sort: 'createdAt', params: {
start: page * limit count: limit,
}, sort: 'createdAt',
timeout: getDefaultTimeoutMilliseconds() start: page * limit
}) },
timeout: getDefaultTimeoutMilliseconds()
}
)
assertSuccessJsonResponse(response) assertSuccessJsonResponse(response)
const responseData = schema.parse(response.data) const responseData = schema.parse(response.data)
const hosts = new Set<string>() const hosts = new Set<string>()
responseData.data.forEach(item => { responseData.data.forEach((item) => {
hosts.add(item.follower.host) hosts.add(item.follower.host)
hosts.add(item.following.host) hosts.add(item.following.host)
}) })

Wyświetl plik

@ -36,7 +36,10 @@ const schema = z.object({
) )
}) })
export const retrieveVideoChannels:FeedProviderMethod = async (domain, page) => { export const retrieveVideoChannels: FeedProviderMethod = async (
domain,
page
) => {
const response = await axios.get(`https://${domain}/api/v1/video-channels`, { const response = await axios.get(`https://${domain}/api/v1/video-channels`, {
params: { params: {
count: limit, count: limit,
@ -51,17 +54,18 @@ export const retrieveVideoChannels:FeedProviderMethod = async (domain, page) =>
throw new NoMoreFeedsError('channel') throw new NoMoreFeedsError('channel')
} }
return responseData.data return responseData.data
.filter(item => item.host === domain) .filter((item) => item.host === domain)
.map((item):FeedData => { .map((item): FeedData => {
const fields:FieldData[] = item.support const fields: FieldData[] =
? [{ name: 'support', value: item.support, verifiedAt: undefined }] item.support !== null
: [] ? [{ name: 'support', value: item.support, verifiedAt: undefined }]
: []
return { return {
name: item.name, name: item.name,
url: item.url ?? `https://${domain}/video-channels/${item.name}/`, url: item.url ?? `https://${domain}/video-channels/${item.name}/`,
avatar: parseAvatarUrl(item.avatar, domain), avatar: parseAvatarUrl(item.avatar, domain),
locked: false, locked: false,
fields: fields, fields,
description: parseDescription(item.description), description: parseDescription(item.description),
displayName: item.displayName, displayName: item.displayName,
followersCount: item.followersCount, followersCount: item.followersCount,

Wyświetl plik

@ -2,9 +2,9 @@ import { NodeProvider } from './NodeProvider'
import { FeedProvider } from './FeedProvider' import { FeedProvider } from './FeedProvider'
export interface Provider { export interface Provider {
getKey(): string getKey: () => string
getNodeProviders(): NodeProvider[] getNodeProviders: () => NodeProvider[]
getFeedProviders(): FeedProvider[] getFeedProviders: () => FeedProvider[]
} }

Wyświetl plik

@ -1,7 +1,7 @@
export class ProviderKeyAlreadyRegisteredError extends Error { export class ProviderKeyAlreadyRegisteredError extends Error {
private readonly _key:string private readonly _key: string
public constructor (key:string) { public constructor (key: string) {
super(`Provider with the key ${key} is already registered`) super(`Provider with the key ${key} is already registered`)
this._key = key this._key = key
} }

Wyświetl plik

@ -0,0 +1,12 @@
export class ProviderKeyNotFoundError extends Error {
private readonly _key: string
public constructor (key: string) {
super(`Provider with the key ${key} is not registered`)
this._key = key
}
public get key (): string {
return this._key
}
}

Wyświetl plik

@ -1,10 +1,8 @@
import { Provider } from './Provider' import { Provider } from './Provider'
import { Dictionary } from 'typescript-collections' import { Dictionary } from 'typescript-collections'
import { ProviderKeyAlreadyRegisteredError } from './ProviderKeyAlreadyRegisteredError' import { ProviderKeyAlreadyRegisteredError } from './ProviderKeyAlreadyRegisteredError'
import { ProviderKeyNotFoundError } from './ProviderKeyNotFoundError'
export interface ProviderCallback { export type ProviderCallback = (key: string, provider: Provider) => void
(key: string, provider: Provider): void
}
const providers: Dictionary<string, Provider> = new Dictionary<string, Provider>() const providers: Dictionary<string, Provider> = new Dictionary<string, Provider>()
@ -14,14 +12,18 @@ const registerProvider = (provider: Provider): void => {
throw new ProviderKeyAlreadyRegisteredError(key) throw new ProviderKeyAlreadyRegisteredError(key)
} }
providers.setValue(key, provider) providers.setValue(key, provider)
console.info('Added provider to registry', { key: key }) console.info('Added provider to registry', { key })
} }
const getProviderByKey = (key: string): Provider => { const getProviderByKey = (key: string): Provider => {
return providers.getValue(key) const provider = providers.getValue(key)
if (provider === undefined) {
throw new ProviderKeyNotFoundError(key)
}
return provider
} }
const getKeys = ():string[] => { const getKeys = (): string[] => {
return providers.keys() return providers.keys()
} }
@ -29,19 +31,19 @@ const forEachProvider = (callback: ProviderCallback): void => {
return providers.forEach(callback) return providers.forEach(callback)
} }
const containsKey = (key:string):boolean => { const containsKey = (key: string): boolean => {
return providers.containsKey(key) return providers.containsKey(key)
} }
export interface ProviderRegistry { export interface ProviderRegistry {
registerProvider: (provider: Provider)=> void, registerProvider: (provider: Provider) => void
getProviderByKey:(key: string)=> Provider getProviderByKey: (key: string) => Provider
forEachProvider:(callback: ProviderCallback)=> void forEachProvider: (callback: ProviderCallback) => void
getKeys:()=>string[] getKeys: () => string[]
containsKey:(key:string)=>boolean containsKey: (key: string) => boolean
} }
export const providerRegistry:ProviderRegistry = { export const providerRegistry: ProviderRegistry = {
registerProvider, registerProvider,
getProviderByKey, getProviderByKey,
forEachProvider, forEachProvider,

Wyświetl plik

@ -1,20 +1,22 @@
import { UnexpectedResponseError } from './UnexpectedResponseError' import { UnexpectedResponseError } from './UnexpectedResponseError'
export class UnexpectedContentTypeError extends UnexpectedResponseError { export class UnexpectedContentTypeError extends UnexpectedResponseError {
private readonly _expectedContentType: string private readonly _expectedContentType: string
private readonly _actualContentType: string private readonly _actualContentType: string
public constructor (actualContentType: string, expectedContentType:string) { public constructor (actualContentType: string, expectedContentType: string) {
super(`Expected content type '${expectedContentType}' but got '${actualContentType}'`) super(
this._expectedContentType = expectedContentType `Expected content type '${expectedContentType}' but got '${actualContentType}'`
this._actualContentType = actualContentType )
} this._expectedContentType = expectedContentType
this._actualContentType = actualContentType
}
get expectedContentType (): string { get expectedContentType (): string {
return this._expectedContentType return this._expectedContentType
} }
get actualContentType (): string { get actualContentType (): string {
return this._actualContentType return this._actualContentType
} }
} }

Wyświetl plik

@ -1,3 +1 @@
export class UnexpectedResponseError extends Error { export class UnexpectedResponseError extends Error {}
}

Wyświetl plik

@ -1,20 +1,22 @@
import { UnexpectedResponseError } from './UnexpectedResponseError' import { UnexpectedResponseError } from './UnexpectedResponseError'
export class UnexpectedResponseStatusError extends UnexpectedResponseError { export class UnexpectedResponseStatusError extends UnexpectedResponseError {
private readonly _expectedStatusCode: number private readonly _expectedStatusCode: number
private readonly _actualStatusCode: number private readonly _actualStatusCode: number
public constructor (expectedStatusCode:number, actualStatusCode:number) { public constructor (expectedStatusCode: number, actualStatusCode: number) {
super(`Expected response code ${expectedStatusCode} but got ${actualStatusCode}`) super(
this._actualStatusCode = actualStatusCode `Expected response code ${expectedStatusCode} but got ${actualStatusCode}`
this._expectedStatusCode = expectedStatusCode )
} this._actualStatusCode = actualStatusCode
this._expectedStatusCode = expectedStatusCode
}
get expectedStatusCode (): number { get expectedStatusCode (): number {
return this._expectedStatusCode return this._expectedStatusCode
} }
get actualStatusCode (): number { get actualStatusCode (): number {
return this._actualStatusCode return this._actualStatusCode
} }
} }

Wyświetl plik

@ -2,7 +2,9 @@ import { AxiosResponse } from 'axios'
import { UnexpectedResponseStatusError } from './UnexpectedResponseStatusError' import { UnexpectedResponseStatusError } from './UnexpectedResponseStatusError'
import { UnexpectedContentTypeError } from './UnexpectedContentTypeError' import { UnexpectedContentTypeError } from './UnexpectedContentTypeError'
export const assertSuccessJsonResponse = (response: AxiosResponse<unknown>): void => { export const assertSuccessJsonResponse = (
response: AxiosResponse<unknown>
): void => {
const expectedStatus = 200 const expectedStatus = 200
const actualStatus = response.status const actualStatus = response.status
if (actualStatus !== expectedStatus) { if (actualStatus !== expectedStatus) {

Wyświetl plik

@ -1,3 +1,3 @@
export const getDefaultTimeoutMilliseconds = () :number => { export const getDefaultTimeoutMilliseconds = (): number => {
return parseInt(process.env.DEFAULT_TIMEOUT_MILLISECONDS ?? '10000') return parseInt(process.env.DEFAULT_TIMEOUT_MILLISECONDS ?? '10000')
} }

Wyświetl plik

@ -3,13 +3,20 @@ import { updateNodeIps } from '../../Storage/Nodes/updateNodeIps'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
const refreshNodeIps = async (elastic: ElasticClient, node:Node):Promise<Node> => { const refreshNodeIps = async (
elastic: ElasticClient,
node: Node
): Promise<Node> => {
console.info('Looking up node ip addresses', { console.info('Looking up node ip addresses', {
nodeDomain: node.domain nodeDomain: node.domain
}) })
try { try {
const addresses = await lookup(node.domain, { all: true }) const addresses = await lookup(node.domain, { all: true })
return updateNodeIps(elastic, node, addresses.map(resolution => resolution.address)) return await updateNodeIps(
elastic,
node,
addresses.map((resolution) => resolution.address)
)
} catch (error) { } catch (error) {
console.warn('Could not lookup the domain', { node, error }) console.warn('Could not lookup the domain', { node, error })
return node return node

Wyświetl plik

@ -7,9 +7,17 @@ import Feed from '../../Storage/Definitions/Feed'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const addFeed = async (elastic: ElasticClient, node: Node, feedData: FeedData): Promise<Feed> => { export const addFeed = async (
elastic: ElasticClient,
node: Node,
feedData: FeedData
): Promise<Feed> => {
const fulltext = prepareFulltext(feedData, node) const fulltext = prepareFulltext(feedData, node)
const extractedTags = extractTags(fulltext) const extractedTags = extractTags(fulltext)
const extractedEmails = extractEmails(fulltext) const extractedEmails = extractEmails(fulltext)
return await createFeed(elastic, { ...feedData, extractedTags, extractedEmails }, node) return await createFeed(
elastic,
{ ...feedData, extractedTags, extractedEmails },
node
)
} }

Wyświetl plik

@ -2,13 +2,21 @@ import { FeedData } from '../../Fediverse/Providers/FeedData'
import striptags from 'striptags' import striptags from 'striptags'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
export default function (feedData: FeedData, node: Node):string { export default function (feedData: FeedData, node: Node): string {
return striptags( return striptags(
feedData.displayName + feedData.displayName +
' ' + feedData.description + ' ' +
' ' + feedData.fields.map(field => field.name).join(' ') + feedData.description +
' ' + feedData.fields.map(field => field.value).join(' ') + ' ' +
' ' + feedData.name + '@' + node.domain + feedData.fields.map((field) => field.name).join(' ') +
(feedData.parentFeed ? (' ' + feedData.parentFeed.name + '@' + feedData.parentFeed.hostDomain) : '') ' ' +
feedData.fields.map((field) => field.value).join(' ') +
' ' +
feedData.name +
'@' +
node.domain +
(feedData.parentFeed != null
? ' ' + feedData.parentFeed.name + '@' + feedData.parentFeed.hostDomain
: '')
) )
} }

Wyświetl plik

@ -7,11 +7,20 @@ import Node from '../../Storage/Definitions/Node'
import prepareFulltext from './prepareFulltext' import prepareFulltext from './prepareFulltext'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const refreshFeed = async (elastic: ElasticClient, feed:Feed, feedData: FeedData, node: Node): Promise<Feed> => { export const refreshFeed = async (
elastic: ElasticClient,
feed: Feed,
feedData: FeedData,
node: Node
): Promise<Feed> => {
const fulltext = prepareFulltext(feedData, node) const fulltext = prepareFulltext(feedData, node)
const extractedTags = extractTags(fulltext) const extractedTags = extractTags(fulltext)
const extractedEmails = extractEmails(fulltext) const extractedEmails = extractEmails(fulltext)
return await updateFeed(elastic, feed, { ...feedData, extractedTags, extractedEmails }) return await updateFeed(elastic, feed, {
...feedData,
extractedTags,
extractedEmails
})
} }

Wyświetl plik

@ -3,13 +3,26 @@ import { FeedProvider } from '../../Fediverse/Providers/FeedProvider'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const refreshFeeds = async (elastic: ElasticClient, provider:FeedProvider, node:Node):Promise<void> => { export const refreshFeeds = async (
elastic: ElasticClient,
provider: FeedProvider,
node: Node
): Promise<void> => {
try { try {
// noinspection InfiniteLoopJS
for (let page = 0; true; page++) { for (let page = 0; true; page++) {
console.info('Retrieve feeds page', { nodeDomain: node.domain, provider: provider.getKey(), page: page }) console.info('Retrieve feeds page', {
nodeDomain: node.domain,
provider: provider.getKey(),
page
})
await refreshFeedsOnPage(elastic, provider, node, page) await refreshFeedsOnPage(elastic, provider, node, page)
} }
} catch (e) { } catch (error) {
console.info('Feed search finished: ' + e, { nodeDomain: node.domain, provider: provider.getKey() }) console.info('Feed search finished', {
error,
nodeDomain: node.domain,
provider: provider.getKey()
})
} }
} }

Wyświetl plik

@ -4,10 +4,23 @@ import Node from '../../Storage/Definitions/Node'
import Feed from '../../Storage/Definitions/Feed' import Feed from '../../Storage/Definitions/Feed'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const refreshFeedsOnPage = async (elastic: ElasticClient, provider:FeedProvider, node:Node, page:number):Promise<Feed[]> => { export const refreshFeedsOnPage = async (
elastic: ElasticClient,
provider: FeedProvider,
node: Node,
page: number
): Promise<Feed[]> => {
const feedData = await provider.retrieveFeeds(node.domain, page) const feedData = await provider.retrieveFeeds(node.domain, page)
console.info('Retrieved feeds', { count: feedData.length, domain: node.domain, provider: provider.getKey(), page: page }) console.info('Retrieved feeds', {
return Promise.all(feedData.map( count: feedData.length,
feedDataItem => refreshOrAddFeed(elastic, node, feedDataItem) domain: node.domain,
)) provider: provider.getKey(),
page
})
return await Promise.all(
feedData.map(
async (feedDataItem) =>
await refreshOrAddFeed(elastic, node, feedDataItem)
)
)
} }

Wyświetl plik

@ -6,17 +6,27 @@ import Node from '../../Storage/Definitions/Node'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
import getFeed from '../../Storage/Feeds/getFeed' import getFeed from '../../Storage/Feeds/getFeed'
export const refreshOrAddFeed = async (elastic: ElasticClient, node:Node, feedData:FeedData):Promise<Feed> => { export const refreshOrAddFeed = async (
let feed:Feed|null = null elastic: ElasticClient,
node: Node,
feedData: FeedData
): Promise<Feed> => {
let feed: Feed | null | undefined
try { try {
feed = await getFeed(elastic, `${feedData.name}@${node.domain}`) feed = await getFeed(elastic, `${feedData.name}@${node.domain}`)
} catch (e) { } catch (e) {}
if (feed !== null && feed !== undefined) {
} console.info('Refreshing feed', {
if (feed) { nodeDomain: node.domain,
console.info('Refreshing feed', { nodeDomain: node.domain, feedName: feedData.name, feedType: feedData.type }) feedName: feedData.name,
feedType: feedData.type
})
return await refreshFeed(elastic, feed, feedData, node) return await refreshFeed(elastic, feed, feedData, node)
} }
console.info('Adding feed', { nodeDomain: node.domain, feedName: feedData.name, feedType: feedData.type }) console.info('Adding feed', {
nodeDomain: node.domain,
feedName: feedData.name,
feedType: feedData.type
})
return await addFeed(elastic, node, feedData) return await addFeed(elastic, node, feedData)
} }

Wyświetl plik

@ -3,13 +3,16 @@ import { updateNodeInfo } from '../../Storage/Nodes/updateNodeInfo'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const refreshNodeInfo = async (elastic: ElasticClient, node:Node):Promise<Node> => { export const refreshNodeInfo = async (
elastic: ElasticClient,
node: Node
): Promise<Node> => {
console.info('Updating info of node', { nodeDomain: node.domain }) console.info('Updating info of node', { nodeDomain: node.domain })
try { try {
const nodeInfo = await retrieveDomainNodeInfo(node.domain) const nodeInfo = await retrieveDomainNodeInfo(node.domain)
return await updateNodeInfo(elastic, node, nodeInfo) return await updateNodeInfo(elastic, node, nodeInfo)
} catch (error) { } catch (error) {
console.warn('Failed to update node info: ' + error) console.warn('Failed to update node info', error)
return node return node
} }
} }

Wyświetl plik

@ -3,13 +3,25 @@ import { findNewNodesOnPage } from './findNewNodesOnPage'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const findNewNodes = async (elastic: ElasticClient, provider:NodeProvider, node:Node):Promise<void> => { export const findNewNodes = async (
elastic: ElasticClient,
provider: NodeProvider,
node: Node
): Promise<void> => {
try { try {
// noinspection InfiniteLoopJS
for (let page = 0; true; page++) { for (let page = 0; true; page++) {
console.info('Retrieve node page', { domain: node.domain, provider: provider.getKey() }) console.info('Retrieve node page', {
domain: node.domain,
provider: provider.getKey()
})
await findNewNodesOnPage(elastic, provider, node, page) await findNewNodesOnPage(elastic, provider, node, page)
} }
} catch (e) { } catch (error) {
console.info('Node search finished: ' + e, { domain: node.domain, provider: provider.getKey() }) console.info('Node search finished', {
error,
domain: node.domain,
provider: provider.getKey()
})
} }
} }

Wyświetl plik

@ -5,10 +5,18 @@ import { ElasticClient } from '../../Storage/ElasticClient'
import isDomainNotBanned from '../../Storage/Nodes/isDomainNotBanned' import isDomainNotBanned from '../../Storage/Nodes/isDomainNotBanned'
export const findNewNodesOnPage = async ( export const findNewNodesOnPage = async (
elastic: ElasticClient, provider: NodeProvider, node:Node, page:number elastic: ElasticClient,
):Promise<number> => { provider: NodeProvider,
node: Node,
page: number
): Promise<number> => {
let domains = await provider.retrieveNodes(node.domain, page) let domains = await provider.retrieveNodes(node.domain, page)
domains = domains.filter(isDomainNotBanned) domains = domains.filter(isDomainNotBanned)
console.log('Found nodes', { count: domains.length, domain: node.domain, provider: provider.getKey(), page: page }) console.log('Found nodes', {
count: domains.length,
domain: node.domain,
provider: provider.getKey(),
page
})
return await createMissingNodes(elastic, domains, node.domain) return await createMissingNodes(elastic, domains, node.domain)
} }

Wyświetl plik

@ -3,12 +3,15 @@ import { setNodeStats } from '../../Storage/Nodes/setNodeStats'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
import Node from '../../Storage/Definitions/Node' import Node from '../../Storage/Definitions/Node'
export type NodeStats ={ export interface NodeStats {
account: number, account: number
channel: number, channel: number
} }
export default async function updateNodeFeedStats (elasticClient: ElasticClient, node: Node) { export default async function updateNodeFeedStats (
elasticClient: ElasticClient,
node: Node
): Promise<void> {
await setNodeStats( await setNodeStats(
elasticClient, elasticClient,
node, node,

Wyświetl plik

@ -1,8 +1,11 @@
import { createMissingNodes } from '../../Storage/Nodes/createMissingNodes' import { createMissingNodes } from '../../Storage/Nodes/createMissingNodes'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export const addNodeSeed = async (elastic: ElasticClient, domains:string[]):Promise<boolean> => { export const addNodeSeed = async (
console.info('Trying to add seed domain nodes', { domains: domains }) elastic: ElasticClient,
domains: string[]
): Promise<boolean> => {
console.info('Trying to add seed domain nodes', { domains })
const result = await createMissingNodes(elastic, domains, undefined) const result = await createMissingNodes(elastic, domains, undefined)
return result > 0 return result > 0
} }

Wyświetl plik

@ -2,10 +2,13 @@ import { deleteDomainFeeds } from '../../Storage/Feeds/deleteDomainFeeds'
import { deleteDomainNodes } from '../../Storage/Nodes/deleteDomainNodes' import { deleteDomainNodes } from '../../Storage/Nodes/deleteDomainNodes'
import { ElasticClient } from '../../Storage/ElasticClient' import { ElasticClient } from '../../Storage/ElasticClient'
export default async function deleteDomains (elastic: ElasticClient, domains:string[]):Promise<number> { export default async function deleteDomains (
if (domains === []) { elastic: ElasticClient,
return domains: string[]
): Promise<number> {
if (domains.length === 0) {
return 0
} }
await deleteDomainFeeds(elastic, domains) await deleteDomainFeeds(elastic, domains)
return deleteDomainNodes(elastic, domains) return await deleteDomainNodes(elastic, domains)
} }

Wyświetl plik

@ -1,7 +1,7 @@
export default function getBannedDomains ():string[] { export default function getBannedDomains (): string[] {
const domains = process.env.BANNED_DOMAINS ?? '' const domains = process.env.BANNED_DOMAINS ?? ''
if (domains === '') { if (domains === '') {
return [] return []
} }
return domains.split(',').map(domain => domain.toLowerCase()) return domains.split(',').map((domain) => domain.toLowerCase())
} }

Wyświetl plik

@ -12,7 +12,10 @@ import refreshNodeIps from './Dns/refreshNodeIps'
import { ElasticClient } from '../Storage/ElasticClient' import { ElasticClient } from '../Storage/ElasticClient'
import updateNodeFeedStats from './Nodes/updateNodeFeedStats' import updateNodeFeedStats from './Nodes/updateNodeFeedStats'
export const processNextNode = async (elastic: ElasticClient, providerRegistry:ProviderRegistry):Promise<void> => { export const processNextNode = async (
elastic: ElasticClient,
providerRegistry: ProviderRegistry
): Promise<void> => {
console.info('#############################################') console.info('#############################################')
let node = await fetchNodeToProcess(elastic) let node = await fetchNodeToProcess(elastic)
node = await setNodeRefreshAttempted(elastic, node) node = await setNodeRefreshAttempted(elastic, node)
@ -20,25 +23,35 @@ export const processNextNode = async (elastic: ElasticClient, providerRegistry:P
node = await refreshNodeIps(elastic, node) node = await refreshNodeIps(elastic, node)
node = await refreshNodeInfo(elastic, node) node = await refreshNodeInfo(elastic, node)
if (!providerRegistry.containsKey(node.softwareName)) { const softwareName = node.softwareName ?? ''
console.warn('Unknown software', { domain: node.domain, software: node.softwareName }) if (!providerRegistry.containsKey(softwareName)) {
console.warn('Unknown software', {
domain: node.domain,
software: node.softwareName
})
await deleteOldFeeds(elastic, node) await deleteOldFeeds(elastic, node)
await setNodeRefreshed(elastic, node) await setNodeRefreshed(elastic, node)
return return
} }
const provider = providerRegistry.getProviderByKey(node.softwareName) const provider = providerRegistry.getProviderByKey(softwareName)
await Promise.all( await Promise.all(
provider.getNodeProviders().map((nodeProvider:NodeProvider) => { provider.getNodeProviders().map(async (nodeProvider: NodeProvider) => {
console.info('Searching for nodes', { domain: node.domain, provider: nodeProvider.getKey() }) console.info('Searching for nodes', {
return findNewNodes(elastic, nodeProvider, node) domain: node.domain,
provider: nodeProvider.getKey()
})
return await findNewNodes(elastic, nodeProvider, node)
}) })
) )
await Promise.all( await Promise.all(
provider.getFeedProviders().map((feedProvider:FeedProvider) => { provider.getFeedProviders().map(async (feedProvider: FeedProvider) => {
console.info('Searching for feeds', { domain: node.domain, provider: feedProvider.getKey() }) console.info('Searching for feeds', {
return refreshFeeds(elastic, feedProvider, node) domain: node.domain,
provider: feedProvider.getKey()
})
return await refreshFeeds(elastic, feedProvider, node)
}) })
) )

Wyświetl plik

@ -1,29 +1,29 @@
import Field from './Field' import Field from './Field'
interface Feed { interface Feed {
domain: string domain: string
foundAt: number, foundAt: number
refreshedAt?: number, refreshedAt?: number
name: string, name: string
fullName: string, fullName: string
displayName: string, displayName: string
description: string, description: string
strippedDescription?: string, strippedDescription?: string
followersCount?: number, followersCount?: number
followingCount?: number, followingCount?: number
statusesCount?: number, statusesCount?: number
lastStatusAt?: number, lastStatusAt?: number
createdAt?: number, createdAt?: number
bot?: boolean, bot?: boolean
locked: boolean, locked: boolean
url: string, url: string
avatar?: string, avatar?: string
type: 'account' | 'channel', type: 'account' | 'channel'
parentFeedName?: string, parentFeedName?: string
parentFeedDomain?: string parentFeedDomain?: string
fields: Field[], fields: Field[]
extractedEmails: string[], extractedEmails: string[]
extractedTags: string[] extractedTags: string[]
} }
export default Feed export default Feed

Wyświetl plik

@ -1,8 +1,8 @@
interface Field { interface Field {
name: string, name: string
value: string value: string
strippedName?: string strippedName?: string
strippedValue?: string strippedValue?: string
} }
export default Field export default Field

Wyświetl plik

@ -1,13 +1,13 @@
interface Geo { interface Geo {
cityName?: string, cityName?: string
continentName?: string, continentName?: string
countryIsoCode?: string, countryIsoCode?: string
countryName?: string, countryName?: string
latitude: number latitude: number
longitude: number longitude: number
location?: string, location?: string
regionIsoCode?: string, regionIsoCode?: string
regionName?: string regionName?: string
} }
export default Geo export default Geo

Wyświetl plik

@ -1,25 +1,25 @@
import Geo from './Geo' import Geo from './Geo'
interface Node { interface Node {
name?:string, name?: string
strippedName?:string, strippedName?: string
foundAt: number, foundAt: number
refreshAttemptedAt?: number refreshAttemptedAt?: number
refreshedAt?: number refreshedAt?: number
openRegistrations?: boolean openRegistrations?: boolean
domain: string, domain: string
serverIps?: string[], serverIps?: string[]
geoip?: Geo[], geoip?: Geo[]
softwareName?: string; softwareName?: string
softwareVersion?: string softwareVersion?: string
standardizedSoftwareVersion?: string standardizedSoftwareVersion?: string
halfYearActiveUserCount?: number, halfYearActiveUserCount?: number
monthActiveUserCount?: number, monthActiveUserCount?: number
statusesCount?: number, statusesCount?: number
totalUserCount?: number, totalUserCount?: number
discoveredByDomain?:string, discoveredByDomain?: string
accountFeedCount?: number, accountFeedCount?: number
channelFeedCount?: number, channelFeedCount?: number
} }
export default Node export default Node

Wyświetl plik

@ -6,7 +6,7 @@ const elasticClient = new Client({
}, },
auth: { auth: {
username: process.env.ELASTIC_USER ?? 'elastic', username: process.env.ELASTIC_USER ?? 'elastic',
password: process.env.ELASTIC_PASSWORD password: process.env.ELASTIC_PASSWORD ?? ''
} }
}) })

Wyświetl plik

@ -1,6 +1,6 @@
import { FeedData } from '../../Fediverse/Providers/FeedData' import { FeedData } from '../../Fediverse/Providers/FeedData'
export default interface StorageFeedData extends FeedData{ export default interface StorageFeedData extends FeedData {
extractedTags:string[], extractedTags: string[]
extractedEmails:string[], extractedEmails: string[]
} }

Wyświetl plik

@ -9,7 +9,7 @@ const assertFeedIndex = async (elastic: ElasticClient): Promise<void> => {
await elastic.ingest.putPipeline({ await elastic.ingest.putPipeline({
id: 'feed', id: 'feed',
description: 'Default feed pipeline', description: 'Default feed pipeline',
processors: processors processors
}) })
console.info('Checking feed index') console.info('Checking feed index')
const exists = await elastic.indices.exists({ const exists = await elastic.indices.exists({
@ -31,12 +31,8 @@ const assertFeedIndex = async (elastic: ElasticClient): Promise<void> => {
html: { html: {
type: 'custom', type: 'custom',
tokenizer: 'standard', tokenizer: 'standard',
filter: [ filter: ['lowercase'],
'lowercase' char_filter: ['html_strip']
],
char_filter: [
'html_strip'
]
} }
} }
} }

Wyświetl plik

@ -3,15 +3,18 @@ import Feed from '../Definitions/Feed'
import feedIndex from '../Definitions/feedIndex' import feedIndex from '../Definitions/feedIndex'
import { NodeStats } from '../../Jobs/Nodes/updateNodeFeedStats' import { NodeStats } from '../../Jobs/Nodes/updateNodeFeedStats'
type Aggregation = { interface Aggregation {
buckets:{ buckets: Array<{
key:'account'|'channel', key: 'account' | 'channel'
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
doc_count:number doc_count: number
}[] }>
} }
export default async function countNodeFeeds (elastic: ElasticClient, domain:string):Promise<NodeStats> { export default async function countNodeFeeds (
elastic: ElasticClient,
domain: string
): Promise<NodeStats> {
await elastic.indices.refresh({ index: feedIndex }) await elastic.indices.refresh({ index: feedIndex })
const response = await elastic.search<Feed>({ const response = await elastic.search<Feed>({
index: feedIndex, index: feedIndex,
@ -27,12 +30,12 @@ export default async function countNodeFeeds (elastic: ElasticClient, domain:str
} }
} }
}) })
const types = response.aggregations.types as Aggregation const types = response?.aggregations?.types as Aggregation
const result:NodeStats = { const result: NodeStats = {
channel: 0, channel: 0,
account: 0 account: 0
} }
types.buckets.forEach(item => { types.buckets.forEach((item) => {
result[item.key] += item.doc_count result[item.key] += item.doc_count
}) })
return result return result

Wyświetl plik

@ -4,8 +4,13 @@ import feedIndex from '../Definitions/feedIndex'
import getFeed from './getFeed' import getFeed from './getFeed'
import Feed from '../Definitions/Feed' import Feed from '../Definitions/Feed'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
import assertDefined from '../assertDefined'
export const createFeed = async (elastic: ElasticClient, feedData: StorageFeedData, node: Node): Promise<Feed> => { export const createFeed = async (
elastic: ElasticClient,
feedData: StorageFeedData,
node: Node
): Promise<Feed> => {
const fullName = `${feedData.name}@${node.domain}` const fullName = `${feedData.name}@${node.domain}`
await elastic.create<Feed>({ await elastic.create<Feed>({
index: feedIndex, index: feedIndex,
@ -25,8 +30,8 @@ export const createFeed = async (elastic: ElasticClient, feedData: StorageFeedDa
displayName: feedData.displayName, displayName: feedData.displayName,
locked: feedData.locked, locked: feedData.locked,
createdAt: feedData.createdAt.getTime(), createdAt: feedData.createdAt.getTime(),
foundAt: (new Date()).getTime(), foundAt: new Date().getTime(),
fields: feedData.fields.map(field => { fields: feedData.fields.map((field) => {
return { name: field.name, value: field.value } return { name: field.name, value: field.value }
}), }),
extractedEmails: feedData.extractedEmails, extractedEmails: feedData.extractedEmails,
@ -36,6 +41,12 @@ export const createFeed = async (elastic: ElasticClient, feedData: StorageFeedDa
type: feedData.type type: feedData.type
} }
}) })
console.info('Created new feed', { feedName: feedData.name, nodeDomain: node.domain }) console.info('Created new feed', {
return getFeed(elastic, fullName) feedName: feedData.name,
nodeDomain: node.domain
})
return assertDefined(
await getFeed(elastic, fullName),
'Missing feed after creating it'
)
} }

Wyświetl plik

@ -1,13 +1,16 @@
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import feedIndex from '../Definitions/feedIndex' import feedIndex from '../Definitions/feedIndex'
export const deleteDomainFeeds = async (elastic: ElasticClient, domains:string[]): Promise<number> => { export const deleteDomainFeeds = async (
elastic: ElasticClient,
domains: string[]
): Promise<number> => {
await elastic.indices.refresh({ index: feedIndex }) await elastic.indices.refresh({ index: feedIndex })
const result = await elastic.deleteByQuery({ const result = await elastic.deleteByQuery({
index: feedIndex, index: feedIndex,
query: { query: {
bool: { bool: {
should: domains.map(domain => { should: domains.map((domain) => {
return { return {
regexp: { regexp: {
domain: { domain: {
@ -22,7 +25,8 @@ export const deleteDomainFeeds = async (elastic: ElasticClient, domains:string[]
} }
}) })
console.info('Deleted domain feeds', { console.info('Deleted domain feeds', {
count: result.deleted, domains count: result.deleted ?? 0,
domains
}) })
return result.deleted return result.deleted ?? 0
} }

Wyświetl plik

@ -2,7 +2,10 @@ import { ElasticClient } from '../ElasticClient'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
import feedIndex from '../Definitions/feedIndex' import feedIndex from '../Definitions/feedIndex'
export const deleteOldFeeds = async (elastic: ElasticClient, node: Node): Promise<number> => { export const deleteOldFeeds = async (
elastic: ElasticClient,
node: Node
): Promise<number> => {
await elastic.indices.refresh({ index: feedIndex }) await elastic.indices.refresh({ index: feedIndex })
const result = await elastic.deleteByQuery({ const result = await elastic.deleteByQuery({
index: feedIndex, index: feedIndex,
@ -16,7 +19,9 @@ export const deleteOldFeeds = async (elastic: ElasticClient, node: Node): Promis
} }
}) })
console.info('Deleted old feeds', { console.info('Deleted old feeds', {
count: result.deleted, olderThen: node.refreshAttemptedAt, nodeDomain: node.domain count: result.deleted ?? 0,
olderThen: node.refreshAttemptedAt,
nodeDomain: node.domain
}) })
return result.deleted return result.deleted ?? 0
} }

Wyświetl plik

@ -2,7 +2,10 @@ import Feed from '../Definitions/Feed'
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import feedIndex from '../Definitions/feedIndex' import feedIndex from '../Definitions/feedIndex'
const getFeed = async (elastic: ElasticClient, feedFullName:string):Promise<Feed> => { const getFeed = async (
elastic: ElasticClient,
feedFullName: string
): Promise<Feed | undefined> => {
const result = await elastic.get<Feed>({ const result = await elastic.get<Feed>({
index: feedIndex, index: feedIndex,
id: feedFullName id: feedFullName

Wyświetl plik

@ -3,8 +3,13 @@ import Feed from '../Definitions/Feed'
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import feedIndex from '../Definitions/feedIndex' import feedIndex from '../Definitions/feedIndex'
import getFeed from './getFeed' import getFeed from './getFeed'
import assertDefined from '../assertDefined'
export const updateFeed = async (elastic: ElasticClient, feed:Feed, feedData:StorageFeedData):Promise<Feed> => { export const updateFeed = async (
elastic: ElasticClient,
feed: Feed,
feedData: StorageFeedData
): Promise<Feed> => {
await elastic.update<Feed>({ await elastic.update<Feed>({
index: feedIndex, index: feedIndex,
id: feed.fullName, id: feed.fullName,
@ -20,9 +25,9 @@ export const updateFeed = async (elastic: ElasticClient, feed:Feed, feedData:Sto
displayName: feedData.displayName, displayName: feedData.displayName,
locked: feedData.locked, locked: feedData.locked,
createdAt: feedData.createdAt, createdAt: feedData.createdAt,
refreshedAt: (new Date()).getTime(), refreshedAt: new Date().getTime(),
type: feedData.type, type: feedData.type,
fields: feedData.fields.map(field => { fields: feedData.fields.map((field) => {
return { name: field.name, value: field.value } return { name: field.name, value: field.value }
}), }),
extractedEmails: feedData.extractedEmails, extractedEmails: feedData.extractedEmails,
@ -30,5 +35,8 @@ export const updateFeed = async (elastic: ElasticClient, feed:Feed, feedData:Sto
} }
}) })
console.info('Updated feed', { feedName: feed.name, nodeDomain: feed.domain }) console.info('Updated feed', { feedName: feed.name, nodeDomain: feed.domain })
return getFeed(elastic, feed.fullName) return assertDefined(
await getFeed(elastic, feed.fullName),
'Missing updated feed'
)
} }

Wyświetl plik

@ -2,14 +2,14 @@ import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import dateProperty from '../Properties/dateProperty' import dateProperty from '../Properties/dateProperty'
const assertNodeIndex = async (elastic: ElasticClient):Promise<void> => { const assertNodeIndex = async (elastic: ElasticClient): Promise<void> => {
console.info('Setting node pipeline') console.info('Setting node pipeline')
await elastic.ingest.putPipeline({ await elastic.ingest.putPipeline({
id: 'node', id: 'node',
description: 'Default node pipeline', description: 'Default node pipeline',
processors: [ processors: [
{ {
// @ts-ignore // @ts-expect-error
geoip: { geoip: {
ignore_missing: true, ignore_missing: true,
field: 'serverIps', field: 'serverIps',

Wyświetl plik

@ -1,24 +1,32 @@
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
export const createMissingNodes = async (elastic: ElasticClient, domains:string[], discoveredByDomain:string|undefined):Promise<number> => { export const createMissingNodes = async (
elastic: ElasticClient,
domains: string[],
discoveredByDomain: string | undefined
): Promise<number> => {
const response = await elastic.bulk({ const response = await elastic.bulk({
index: nodeIndex, index: nodeIndex,
body: domains.flatMap(domain => [ body: domains.flatMap((domain) => [
{ {
create: { _id: domain } create: { _id: domain }
}, },
{ {
domain: domain, domain,
discoveredByDomain, discoveredByDomain,
foundAt: (new Date()).getTime() foundAt: new Date().getTime()
} }
]) ])
}) })
const createdCount = response.items.filter(item => item.create.status === 201).length const createdCount = response.items.filter(
(item) => item.create?.status === 201
).length
console.warn('Created new nodes', { console.warn('Created new nodes', {
requestedCount: domains.length, requestedCount: domains.length,
createdCount: createdCount, createdCount,
errors: response.items.filter(item => item.create.status !== 201).map(item => item.create.error.reason) errors: response.items
.filter((item) => item.create?.status !== 201)
.map((item) => item.create?.error?.reason)
}) })
return createdCount return createdCount
} }

Wyświetl plik

@ -1,13 +1,16 @@
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
export const deleteDomainNodes = async (elastic: ElasticClient, domains:string[]): Promise<number> => { export const deleteDomainNodes = async (
elastic: ElasticClient,
domains: string[]
): Promise<number> => {
await elastic.indices.refresh({ index: nodeIndex }) await elastic.indices.refresh({ index: nodeIndex })
const result = await elastic.deleteByQuery({ const result = await elastic.deleteByQuery({
index: nodeIndex, index: nodeIndex,
query: { query: {
bool: { bool: {
should: domains.map(domain => { should: domains.map((domain) => {
return { return {
regexp: { regexp: {
domain: { domain: {
@ -22,7 +25,8 @@ export const deleteDomainNodes = async (elastic: ElasticClient, domains:string[]
} }
}) })
console.info('Deleted domain nodes', { console.info('Deleted domain nodes', {
count: result.deleted, domains count: result.deleted ?? 0,
domains
}) })
return result.deleted return result.deleted ?? 0
} }

Wyświetl plik

@ -5,7 +5,9 @@ import Node from '../Definitions/Node'
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
export const fetchNodeToProcess = async (elastic: ElasticClient): Promise<Node> => { export const fetchNodeToProcess = async (
elastic: ElasticClient
): Promise<Node> => {
await elastic.indices.refresh({ index: nodeIndex }) await elastic.indices.refresh({ index: nodeIndex })
let node = await findNotProcessedNodeWithAttemptLimit(elastic) let node = await findNotProcessedNodeWithAttemptLimit(elastic)
if (node !== null) { if (node !== null) {

Wyświetl plik

@ -2,42 +2,55 @@ import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
const findNodeWithOldestRefreshWithLimits = async (elastic: ElasticClient): Promise<Node | null> => { const findNodeWithOldestRefreshWithLimits = async (
elastic: ElasticClient
): Promise<Node | null> => {
const currentTimestamp = Date.now() const currentTimestamp = Date.now()
const attemptLimitMilliseconds = parseInt(process.env.REATTEMPT_MINUTES ?? '60') * 60 * 1000 const attemptLimitMilliseconds =
parseInt(process.env.REATTEMPT_MINUTES ?? '60') * 60 * 1000
const attemptLimitDate = new Date(currentTimestamp - attemptLimitMilliseconds) const attemptLimitDate = new Date(currentTimestamp - attemptLimitMilliseconds)
const refreshLimitMilliseconds = parseInt(process.env.REFRESH_HOURS ?? '168') * 60 * 60 * 1000 const refreshLimitMilliseconds =
parseInt(process.env.REFRESH_HOURS ?? '168') * 60 * 60 * 1000
const refreshLimitDate = new Date(currentTimestamp - refreshLimitMilliseconds) const refreshLimitDate = new Date(currentTimestamp - refreshLimitMilliseconds)
console.log('Searching instance not refreshed for longest time and before refreshLimit and attemptLimit', { console.log(
refreshLimitMilliseconds, 'Searching instance not refreshed for longest time and before refreshLimit and attemptLimit',
refreshLimitDate, {
attemptLimitDate, refreshLimitMilliseconds,
attemptLimitMilliseconds refreshLimitDate,
}) attemptLimitDate,
attemptLimitMilliseconds
}
)
const result = await elastic.search<Node>({ const result = await elastic.search<Node>({
index: nodeIndex, index: nodeIndex,
body: { body: {
size: 1, size: 1,
sort: [{ sort: [
refreshedAt: { order: 'asc' } {
}], refreshedAt: { order: 'asc' }
}
],
query: { query: {
bool: { bool: {
must: [ must: [
{ range: { refreshedAt: { lt: refreshLimitDate.getTime() } } } { range: { refreshedAt: { lt: refreshLimitDate.getTime() } } }
], ],
should: [ should: [
{ range: { refreshAttemptedAt: { lt: attemptLimitDate.getTime() } } }, {
{ bool: { must_not: [{ exists: { field: 'refreshAttemptedAt' } }] } } range: { refreshAttemptedAt: { lt: attemptLimitDate.getTime() } }
},
{
bool: { must_not: [{ exists: { field: 'refreshAttemptedAt' } }] }
}
], ],
minimum_should_match: 1 minimum_should_match: 1
} }
} }
} }
}) })
if (result.hits.hits.length > 0) { const node = result.hits.hits.pop()?._source
const node = result.hits.hits[0]._source if (node !== undefined) {
console.log('Found oldest node', { node }) console.info('Found oldest node', { node })
return node return node
} }
return null return null

Wyświetl plik

@ -2,33 +2,44 @@ import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
const findNotProcessedNodeWithAttemptLimit = async (elastic: ElasticClient): Promise<Node|null> => { const findNotProcessedNodeWithAttemptLimit = async (
elastic: ElasticClient
): Promise<Node | null> => {
const currentTimestamp = Date.now() const currentTimestamp = Date.now()
const attemptLimitMilliseconds = parseInt(process.env.REATTEMPT_MINUTES ?? '60') * 60 * 1000 const attemptLimitMilliseconds =
parseInt(process.env.REATTEMPT_MINUTES ?? '60') * 60 * 1000
const attemptLimitDate = new Date(currentTimestamp - attemptLimitMilliseconds) const attemptLimitDate = new Date(currentTimestamp - attemptLimitMilliseconds)
console.log('Searching for not yet processed node not attempted before attemptLimit', { attemptLimitDate, attemptLimitMilliseconds }) console.log(
'Searching for not yet processed node not attempted before attemptLimit',
{ attemptLimitDate, attemptLimitMilliseconds }
)
const result = await elastic.search<Node>({ const result = await elastic.search<Node>({
index: nodeIndex, index: nodeIndex,
body: { body: {
size: 1, size: 1,
sort: [{ sort: [
foundAt: { order: 'asc' } {
}], foundAt: { order: 'asc' }
}
],
query: { query: {
bool: { bool: {
must_not: [ must_not: [{ exists: { field: 'refreshedAt' } }],
{ exists: { field: 'refreshedAt' } }],
should: [ should: [
{ bool: { must_not: [{ exists: { field: 'refreshAttemptedAt' } }] } }, {
{ range: { refreshAttemptedAt: { lt: attemptLimitDate.getTime() } } } bool: { must_not: [{ exists: { field: 'refreshAttemptedAt' } }] }
},
{
range: { refreshAttemptedAt: { lt: attemptLimitDate.getTime() } }
}
], ],
minimum_should_match: 1 minimum_should_match: 1
} }
} }
} }
}) })
if (result.hits.hits.length > 0) { const node = result.hits.hits.pop()?._source
const node = result.hits.hits[0]._source if (node !== undefined) {
console.log('Found not yet processed node', { node }) console.log('Found not yet processed node', { node })
return node return node
} }

Wyświetl plik

@ -2,7 +2,10 @@ import { ElasticClient } from '../ElasticClient'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
const getNode = async (elastic:ElasticClient, domain:string):Promise<Node> => { const getNode = async (
elastic: ElasticClient,
domain: string
): Promise<Node | undefined> => {
const result = await elastic.get<Node>({ const result = await elastic.get<Node>({
index: nodeIndex, index: nodeIndex,
id: domain id: domain

Wyświetl plik

@ -1,7 +1,9 @@
import getBannedDomains from '../../Jobs/Seed/getBannedDomains' import getBannedDomains from '../../Jobs/Seed/getBannedDomains'
export default function isDomainNotBanned (domain):boolean { export default function isDomainNotBanned (domain): boolean {
return getBannedDomains().filter( return (
banned => domain.match(new RegExp('(.*\\.)?' + banned, 'gi')) !== null getBannedDomains().filter(
).length === 0 (banned) => domain.match(new RegExp('(.*\\.)?' + banned, 'gi')) !== null
).length === 0
)
} }

Wyświetl plik

@ -2,10 +2,14 @@ import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
import getNode from './getNode' import getNode from './getNode'
import assertDefined from '../assertDefined'
export const setNodeRefreshAttempted = async (elastic: ElasticClient, node:Node):Promise<Node> => { export const setNodeRefreshAttempted = async (
elastic: ElasticClient,
node: Node
): Promise<Node> => {
const date = new Date() const date = new Date()
console.info('Setting node refresh attempt', { domain: node.domain, date: date }) console.info('Setting node refresh attempt', { domain: node.domain, date })
await elastic.update<Node>({ await elastic.update<Node>({
index: nodeIndex, index: nodeIndex,
id: node.domain, id: node.domain,
@ -13,5 +17,8 @@ export const setNodeRefreshAttempted = async (elastic: ElasticClient, node:Node)
refreshAttemptedAt: date.getTime() refreshAttemptedAt: date.getTime()
} }
}) })
return getNode(elastic, node.domain) return assertDefined(
await getNode(elastic, node.domain),
'Missing node after updating it'
)
} }

Wyświetl plik

@ -2,10 +2,14 @@ import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
import getNode from './getNode' import getNode from './getNode'
import assertDefined from '../assertDefined'
export const setNodeRefreshed = async (elastic: ElasticClient, node:Node):Promise<Node> => { export const setNodeRefreshed = async (
elastic: ElasticClient,
node: Node
): Promise<Node> => {
const date = new Date() const date = new Date()
console.info('Setting node refreshed', { domain: node.domain, date: date }) console.info('Setting node refreshed', { domain: node.domain, date })
await elastic.update<Node>({ await elastic.update<Node>({
index: nodeIndex, index: nodeIndex,
id: node.domain, id: node.domain,
@ -13,5 +17,8 @@ export const setNodeRefreshed = async (elastic: ElasticClient, node:Node):Promis
refreshedAt: date.getTime() refreshedAt: date.getTime()
} }
}) })
return getNode(elastic, node.domain) return assertDefined(
await getNode(elastic, node.domain),
'Missing node after updating it'
)
} }

Wyświetl plik

@ -3,8 +3,13 @@ import nodeIndex from '../Definitions/nodeIndex'
import Node from '../Definitions/Node' import Node from '../Definitions/Node'
import getNode from './getNode' import getNode from './getNode'
import { NodeStats } from '../../Jobs/Nodes/updateNodeFeedStats' import { NodeStats } from '../../Jobs/Nodes/updateNodeFeedStats'
import assertDefined from '../assertDefined'
export const setNodeStats = async (elastic: ElasticClient, node:Node, stats: NodeStats):Promise<Node> => { export const setNodeStats = async (
elastic: ElasticClient,
node: Node,
stats: NodeStats
): Promise<Node> => {
console.info('Setting node stats', { domain: node.domain, stats }) console.info('Setting node stats', { domain: node.domain, stats })
await elastic.update<Node>({ await elastic.update<Node>({
index: nodeIndex, index: nodeIndex,
@ -14,5 +19,8 @@ export const setNodeStats = async (elastic: ElasticClient, node:Node, stats: Nod
channelFeedCount: stats.channel channelFeedCount: stats.channel
} }
}) })
return getNode(elastic, node.domain) return assertDefined(
await getNode(elastic, node.domain),
'Missing node after updating it'
)
} }

Wyświetl plik

@ -3,15 +3,20 @@ import Node from '../Definitions/Node'
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import getNode from './getNode' import getNode from './getNode'
import assertDefined from '../assertDefined'
const assertPositiveInt = (number:number|undefined):number|undefined => { const assertPositiveInt = (number: number | undefined): number | undefined => {
if (number === undefined) { if (number === undefined) {
return undefined return undefined
} }
return Math.max(0, Math.round(number)) return Math.max(0, Math.round(number))
} }
export const updateNodeInfo = async (elastic: ElasticClient, node: Node, nodeInfo:NodeInfo):Promise<Node> => { export const updateNodeInfo = async (
elastic: ElasticClient,
node: Node,
nodeInfo: NodeInfo
): Promise<Node> => {
await elastic.update<Node>({ await elastic.update<Node>({
index: nodeIndex, index: nodeIndex,
id: node.domain, id: node.domain,
@ -20,14 +25,21 @@ export const updateNodeInfo = async (elastic: ElasticClient, node: Node, nodeInf
openRegistrations: nodeInfo?.openRegistrations, openRegistrations: nodeInfo?.openRegistrations,
softwareName: nodeInfo?.software?.name?.toLocaleLowerCase(), softwareName: nodeInfo?.software?.name?.toLocaleLowerCase(),
softwareVersion: nodeInfo?.software?.version, softwareVersion: nodeInfo?.software?.version,
halfYearActiveUserCount: assertPositiveInt(nodeInfo?.usage?.users?.activeHalfyear), halfYearActiveUserCount: assertPositiveInt(
monthActiveUserCount: assertPositiveInt(nodeInfo?.usage?.users.activeMonth), nodeInfo?.usage?.users?.activeHalfyear
),
monthActiveUserCount: assertPositiveInt(
nodeInfo?.usage?.users?.activeMonth
),
statusesCount: assertPositiveInt(nodeInfo?.usage?.localPosts), statusesCount: assertPositiveInt(nodeInfo?.usage?.localPosts),
totalUserCount: assertPositiveInt(nodeInfo?.usage?.users?.total) totalUserCount: assertPositiveInt(nodeInfo?.usage?.users?.total)
} }
}) })
const resultNode = await getNode(elastic, node.domain) const resultNode = assertDefined(
await getNode(elastic, node.domain),
'Missing node after updating it'
)
console.info('Updated node info', { node }) console.info('Updated node info', { node })
return resultNode return resultNode
} }

Wyświetl plik

@ -2,8 +2,13 @@ import Node from '../Definitions/Node'
import { ElasticClient } from '../ElasticClient' import { ElasticClient } from '../ElasticClient'
import nodeIndex from '../Definitions/nodeIndex' import nodeIndex from '../Definitions/nodeIndex'
import getNode from './getNode' import getNode from './getNode'
import assertDefined from '../assertDefined'
export const updateNodeIps = async (elastic:ElasticClient, node: Node, ips:string[]):Promise<Node> => { export const updateNodeIps = async (
elastic: ElasticClient,
node: Node,
ips: string[]
): Promise<Node> => {
await elastic.update<Node>({ await elastic.update<Node>({
index: nodeIndex, index: nodeIndex,
id: node.domain, id: node.domain,
@ -11,7 +16,10 @@ export const updateNodeIps = async (elastic:ElasticClient, node: Node, ips:strin
serverIps: ips serverIps: ips
} }
}) })
const resultNode = await getNode(elastic, node.domain) const resultNode = assertDefined(
await getNode(elastic, node.domain),
'Missing node after updating it'
)
console.info('Updated node ips', { resultNode }) console.info('Updated node ips', { resultNode })
return resultNode return resultNode
} }

Wyświetl plik

@ -1,5 +1,5 @@
import { MappingProperty } from '@elastic/elasticsearch/lib/api/types' import { MappingProperty } from '@elastic/elasticsearch/lib/api/types'
const dateProperty:MappingProperty = { type: 'date', format: 'epoch_millis' } const dateProperty: MappingProperty = { type: 'date', format: 'epoch_millis' }
export default dateProperty export default dateProperty

Wyświetl plik

@ -0,0 +1,9 @@
export default function assertDefined<T> (
value: T | undefined | null,
errorMessage: string
): T {
if (value === null || value === undefined) {
throw new Error(errorMessage)
}
return value
}

Wyświetl plik

@ -1,4 +1,9 @@
export const extractEmails = (text:string):string[] => { export const extractEmails = (text: string): string[] => {
return (text.match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi) || []) const matches = text.match(
.map(email => email.toLowerCase()) || [] /([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/gi
)
if (matches === null) {
return []
}
return matches.map((email) => email.toLowerCase())
} }

Wyświetl plik

@ -1,4 +1,7 @@
export const extractTags = (text:string):string[] => { export const extractTags = (text: string): string[] => {
return (text.match(/#[a-z0-9_]+/gi) || []) const matches = text.match(/#[a-z0-9_]+/gi)
.map(hashtag => hashtag.substring(1).toLowerCase()) || [] if (matches === null) {
return []
}
return matches.map((hashtag) => hashtag.substring(1).toLowerCase())
} }

Wyświetl plik

@ -7,16 +7,25 @@ import elasticClient from './Storage/ElasticClient'
import deleteDomains from './Jobs/Seed/deleteBannedNodes' import deleteDomains from './Jobs/Seed/deleteBannedNodes'
import getBannedDomains from './Jobs/Seed/getBannedDomains' import getBannedDomains from './Jobs/Seed/getBannedDomains'
const timeout = async (ms: number): Promise<void> => {
return await new Promise((resolve) => setTimeout(resolve, ms))
}
const loop = async (): Promise<void> => { const loop = async (): Promise<void> => {
// noinspection InfiniteLoopJS
while (true) { while (true) {
try { try {
await processNextNode(elasticClient, providerRegistry) await processNextNode(elasticClient, providerRegistry)
} catch (err) { } catch (err) {
console.warn(err) console.warn(err)
const waitForJobMilliseconds = parseInt(process.env.WAIT_FOR_JOB_MINUTES ?? '60') * 60 * 1000 const waitForJobMilliseconds =
console.info('Delaying next node process', { timeoutMilliseconds: waitForJobMilliseconds, timeoutDate: new Date(Date.now() + waitForJobMilliseconds), now: new Date() }) parseInt(process.env.WAIT_FOR_JOB_MINUTES ?? '60') * 60 * 1000
setTimeout(loop, waitForJobMilliseconds) console.info('Delaying next node process', {
return timeoutMilliseconds: waitForJobMilliseconds,
timeoutDate: new Date(Date.now() + waitForJobMilliseconds),
now: new Date()
})
await timeout(waitForJobMilliseconds)
} }
} }
} }
@ -25,9 +34,13 @@ const app = async (): Promise<void> => {
await assertNodeIndex(elasticClient) await assertNodeIndex(elasticClient)
await assertFeedIndex(elasticClient) await assertFeedIndex(elasticClient)
await deleteDomains(elasticClient, getBannedDomains()) await deleteDomains(elasticClient, getBannedDomains())
const seedDomains = (process.env.SEED_NODE_DOMAIN ?? 'mastodon.social').split(',') const seedDomains = (process.env.SEED_NODE_DOMAIN ?? 'mastodon.social').split(
','
)
await addNodeSeed(elasticClient, seedDomains) await addNodeSeed(elasticClient, seedDomains)
setTimeout(loop) await loop()
} }
app() app()
.then(() => console.info('App finished'))
.catch((error) => console.error('App was interrupted', { error }))

Wyświetl plik

@ -7,7 +7,8 @@
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"alwaysStrict": true, "alwaysStrict": true,
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true,
"strictNullChecks": true,
}, },
"files": ["./src/app.ts"] "files": ["./src/app.ts"]
} }

Wyświetl plik

@ -1,11 +0,0 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"no-console": false
},
"rulesDirectory": []
}

3551
application/yarn.lock 100644

Plik diff jest za duży Load Diff