xLog/src/models/home.model.ts

626 lines
16 KiB
TypeScript

import dayjs from "dayjs"
import { indexer } from "@crossbell/indexer"
import { expandCrossbellNote } from "~/lib/expand-unit"
import { ExpandedNote } from "~/lib/types"
import { client } from "~/queries/graphql"
import filter from "../../data/filter.json"
export type FeedType =
| "latest"
| "following"
| "topic"
| "hottest"
| "search"
| "tag"
export type SearchType = "latest" | "hottest"
export async function getFeed({
type,
cursor,
limit = 30,
characterId,
noteIds,
daysInterval,
searchKeyword,
searchType,
tag,
useHTML,
topicIncludeKeywords,
topicIncludeTags,
}: {
type?: FeedType
cursor?: string
limit?: number
characterId?: number
noteIds?: string[]
daysInterval?: number
searchKeyword?: string
searchType?: SearchType
tag?: string
useHTML?: boolean
topicIncludeKeywords?: string[]
topicIncludeTags?: string[]
}) {
if (type === "search" && !searchKeyword) {
type = "latest"
}
switch (type) {
case "latest": {
const result = await client
.query(
`
query getNotes($filter: [Int!]) {
notes(
where: {
characterId: {
notIn: $filter
}
metadata: {
AND: [{
content: {
path: "sources",
array_contains: "xlog"
}
}, {
OR: [{
content: {
path: "tags",
array_starts_with: "page"
}
}, {
content: {
path: "tags",
array_starts_with: "post"
}
}]
}]
},
},
orderBy: [{ createdAt: desc }],
take: ${limit},
${
cursor
? `
cursor: {
note_characterId_noteId_unique: {
characterId: ${cursor.split("_")[0]},
noteId: ${cursor.split("_")[1]}
},
},`
: ""
}
) {
characterId
noteId
character {
handle
metadata {
content
}
}
createdAt
metadata {
uri
content
}
toNote {
characterId
noteId
character {
handle
metadata {
content
}
}
createdAt
metadata {
uri
content
}
}
}
}`,
{
filter: filter.latest,
},
)
.toPromise()
const list = await Promise.all(
result?.data?.notes
.filter((page: any) => {
return !(
page.metadata?.content?.tags?.includes("comment") &&
page.toNote?.metadata?.content?.tags?.includes("comment")
)
})
.map(async (page: any) => {
const isComment = page.metadata?.content?.tags?.includes("comment")
const expand = await expandCrossbellNote({
note: page,
useScore: !isComment,
useHTML,
})
if (isComment && expand.toNote) {
expand.toNote = await expandCrossbellNote({
note: expand.toNote,
useHTML,
})
}
delete expand.metadata?.content.content
return expand
}),
)
return {
list,
cursor: list?.length
? `${list[list.length - 1]?.characterId}_${list[list.length - 1]
?.noteId}`
: undefined,
count: list?.length || 0,
}
}
case "following": {
if (!characterId) {
return {
list: [],
cursor: "",
count: 0,
}
} else {
const result = await indexer.note.getManyOfCharacterFollowing(
characterId,
{
sources: "xlog",
tags: ["post"],
limit: limit,
cursor,
includeCharacter: true,
},
)
const list = await Promise.all(
result.list.map(async (page: any) => {
const expand = await expandCrossbellNote({
note: page,
useHTML,
})
delete expand.metadata?.content.content
return expand
}),
)
return {
list,
cursor: result.cursor,
count: result.count,
}
}
}
case "topic": {
if (!topicIncludeKeywords && !topicIncludeTags && !noteIds) {
return {
list: [],
cursor: "",
count: 0,
}
}
if (noteIds) {
const orString = noteIds
.map(
(note) =>
`{ noteId: { equals: ${
note.split("-")[1]
} }, characterId: { equals: ${note.split("-")[0]}}},`,
)
.join("\n")
const result = await client
.query(
`
query getNotes {
notes(
where: {
OR: [
${orString}
]
},
orderBy: [{ createdAt: desc }],
take: ${limit},
${
cursor
? `
cursor: {
note_characterId_noteId_unique: {
characterId: ${cursor.split("_")[0]},
noteId: ${cursor.split("_")[1]}
},
},`
: ""
}
) {
characterId
noteId
character {
handle
metadata {
content
}
}
createdAt
metadata {
uri
content
}
}
}`,
{},
)
.toPromise()
const list = await Promise.all(
result?.data?.notes.map(async (page: any) => {
const expand = await expandCrossbellNote({
note: page,
useHTML,
})
delete expand.metadata?.content.content
return expand
}),
)
return {
list: list,
cursor: list?.length
? `${list[list.length - 1]?.characterId}_${list[list.length - 1]
?.noteId}`
: undefined,
count: list?.length || 0,
}
} else {
const includeString = [
...(topicIncludeKeywords?.map(
(topicIncludeKeyword) =>
`{ content: { path: "title", string_contains: "${topicIncludeKeyword}" } }, { content: { path: "content", string_contains: "${topicIncludeKeyword}" } }`,
) || []),
...(topicIncludeTags?.map(
(topicIncludeTag) =>
`{ content: { path: "tags", array_contains: "${topicIncludeTag}" } },`,
) || []),
].join("\n")
const result = await client
.query(
`
query getNotes($filter: [Int!]) {
notes(
where: {
characterId: {
notIn: $filter
},
metadata: {
content: {
path: "sources",
array_contains: "xlog"
},
AND: [{
content: {
path: "tags",
array_contains: "post"
}
}, {
OR: [
${includeString}
]
}]
},
},
orderBy: [{ createdAt: desc }],
take: ${limit},
${
cursor
? `
cursor: {
note_characterId_noteId_unique: {
characterId: ${cursor.split("_")[0]},
noteId: ${cursor.split("_")[1]}
},
},`
: ""
}
) {
characterId
noteId
character {
handle
metadata {
content
}
}
createdAt
metadata {
uri
content
}
}
}`,
{
filter: filter.latest,
},
)
.toPromise()
const list = await Promise.all(
result?.data?.notes.map(async (page: any) => {
const expand = await expandCrossbellNote({
note: page,
useHTML,
})
delete expand.metadata?.content.content
return expand
}),
)
return {
list: list,
cursor: list?.length
? `${list[list.length - 1]?.characterId}_${list[list.length - 1]
?.noteId}`
: undefined,
count: list?.length || 0,
}
}
}
case "hottest": {
let time
if (daysInterval) {
time = dayjs().subtract(daysInterval, "day").toISOString()
}
const result = await client
.query(
`
query getNotes($filter: [Int!]) {
notes(
where: {
characterId: {
notIn: $filter
},
${time ? `createdAt: { gt: "${time}" },` : ``}
stat: { viewDetailCount: { gt: 0 } },
metadata: { content: { path: "sources", array_contains: "xlog" }, AND: { content: { path: "tags", array_contains: "post" } } }
},
orderBy: { stat: { viewDetailCount: desc } },
take: 25,
) {
stat {
viewDetailCount
}
characterId
noteId
character {
handle
metadata {
content
}
}
createdAt
metadata {
uri
content
}
}
}`,
{
filter: filter.latest,
},
)
.toPromise()
let list: ExpandedNote[] = await Promise.all(
result?.data?.notes.map(async (page: any) => {
if (daysInterval) {
const secondAgo = dayjs().diff(dayjs(page.createdAt), "second")
page.stat.hotScore =
page.stat.viewDetailCount / Math.max(Math.log10(secondAgo), 1)
}
const expand = await expandCrossbellNote({
note: page,
useHTML,
})
delete expand.metadata?.content.content
return expand
}),
)
if (daysInterval) {
list = list.sort((a, b) => {
if (a.stat?.hotScore && b.stat?.hotScore) {
return b.stat.hotScore - a.stat.hotScore
} else {
return 0
}
})
}
return {
list: list,
cursor: "",
count: list?.length || 0,
}
}
case "search": {
const result = await indexer.search.notes(searchKeyword!, {
sources: ["xlog"],
tags: ["post"],
limit: limit,
cursor,
includeCharacterMetadata: true,
orderBy: searchType === "hottest" ? "viewCount" : "createdAt",
})
const list = await Promise.all(
result.list.map(async (page: any) => {
const expand = await expandCrossbellNote({
note: page,
useStat: false,
useScore: false,
keyword: searchKeyword,
useHTML,
})
delete expand.metadata?.content.content
return expand
}),
)
return {
list,
cursor: result.cursor,
count: result.count,
}
}
case "tag": {
if (!tag) {
return {
list: [],
cursor: "",
count: 0,
}
}
let result = await indexer.note.getMany({
sources: "xlog",
tags: ["post", tag],
limit,
cursor,
includeCharacter: true,
excludeCharacterId: filter.latest,
} as any)
const list = await Promise.all(
result.list.map(async (page: any) => {
const expand = await expandCrossbellNote({
note: page,
useStat: false,
useScore: true,
useHTML,
})
delete expand.metadata?.content.content
return expand
}),
)
return {
list,
cursor: result.cursor,
count: result.count,
}
}
}
}
export const getShowcase = async () => {
const oneMonthAgo = dayjs().subtract(10, "day").toISOString()
const listResponse = await client
.query(
`
query getCharacters($filter: [Int!]) {
characters(
where: {
characterId: {
notIn: $filter
}
notes: {
some: {
stat: { viewDetailCount: { gte: 100 } }
metadata: {
content: { path: "sources", array_contains: "xlog" }
AND: { content: { path: "tags", array_contains: "post" } }
}
}
}
}
) {
characterId
}
}`,
{
filter: filter.latest,
},
)
.toPromise()
const characterList = listResponse.data?.characters.map((c: any) =>
parseInt(c.characterId),
)
const result = await client
.query(
`
query getCharacters($identities: [Int!]) {
characters( where: { characterId: { in: $identities } }, orderBy: [{ updatedAt: desc }] ) {
handle
characterId
metadata {
uri
content
}
}
notes( where: { characterId: { in: $identities }, createdAt: { gt: "${oneMonthAgo}" }, metadata: { content: { path: "sources", array_contains: "xlog" } } }, orderBy: [{ updatedAt: desc }] ) {
characterId
createdAt
}
}`,
{
identities: characterList,
},
)
.toPromise()
result.data?.characters?.forEach((site: any) => {
if (site.metadata?.content) {
site.metadata.content.name = site.metadata?.content?.name || site.handle
} else {
site.metadata.content = {
name: site.handle,
}
}
site.custom_domain =
site.metadata?.content?.attributes?.find(
(a: any) => a.trait_type === "xlog_custom_domain",
)?.value || ""
})
const createdAts: {
[key: string]: string
} = {}
result.data?.notes.forEach((note: any) => {
if (!createdAts[note.characterId + ""]) {
createdAts[note.characterId + ""] = note.createdAt
}
})
const list = Object.keys(createdAts)
.map((characterId: string) => {
const character = result.data?.characters.find(
(c: any) => c.characterId === characterId,
)
return {
...character,
createdAt: createdAts[characterId],
}
})
.sort((a: any, b: any) => {
return b.createdAt > a.createdAt ? 1 : -1
})
.slice(0, 100)
return list
}