xLog/src/models/site.model.ts

514 lines
12 KiB
TypeScript

import { SiteNavigationItem, Profile } from "~/lib/types"
import { nanoid } from "nanoid"
import unidata from "~/queries/unidata.server"
import { toGateway } from "~/lib/ipfs-parser"
import type Unidata from "unidata.js"
import type { Profiles as UniProfiles } from "unidata.js"
import { createClient } from "@urql/core"
import axios from "axios"
import { indexer } from "@crossbell/indexer"
import type { Contract } from "crossbell.js"
import { CharacterOperatorPermission } from "crossbell.js"
import { GeneralAccount } from "@crossbell/connect-kit"
const expandSite = (site: Profile) => {
site.navigation = JSON.parse(
site.metadata?.raw?.attributes?.find(
(a: any) => a.trait_type === "xlog_navigation",
)?.value || "null",
) ||
site.metadata?.raw?.["_xlog_navigation"] ||
site.metadata?.raw?.["_crosslog_navigation"] || [
{ id: nanoid(), label: "Archives", url: "/archives" },
]
site.css =
site.metadata?.raw?.attributes?.find(
(a: any) => a.trait_type === "xlog_css",
)?.value ||
site.metadata?.raw?.["_xlog_css"] ||
site.metadata?.raw?.["_crosslog_css"] ||
""
site.ga =
site.metadata?.raw?.attributes?.find((a: any) => a.trait_type === "xlog_ga")
?.value || ""
site.custom_domain =
site.metadata?.raw?.attributes?.find(
(a: any) => a.trait_type === "xlog_custom_domain",
)?.value || ""
site.name = site.name || site.username
site.description = site.bio
if (site.avatars) {
site.avatars = site.avatars.map((avatar) => toGateway(avatar))
}
if (site.banners) {
site.banners.map((banner) => {
banner.address = toGateway(banner.address)
return banner
})
}
delete site.metadata?.raw
return site
}
export type GetUserSitesParams =
| {
address: string
unidata?: Unidata
}
| {
handle: string
unidata?: Unidata
}
export const getUserSites = async (params: GetUserSitesParams) => {
let profiles: UniProfiles | null = null
try {
const source = "Crossbell Profile"
const filter = { primary: true }
if ("address" in params) {
profiles = await (params.unidata || unidata).profiles.get({
source,
filter,
identity: params.address,
platform: "Ethereum",
})
}
if ("handle" in params) {
profiles = await (params.unidata || unidata).profiles.get({
source,
filter,
identity: params.handle,
platform: "Crossbell",
})
}
} catch (error) {
return null
}
const sites: Profile[] =
profiles?.list?.map((profile) => {
expandSite(profile)
return profile
}) ?? []
return sites.length > 0 ? sites : null
}
export type GetAccountSitesParams = {
handle: string
unidata?: Unidata
}
export const getAccountSites = (
params: GetAccountSitesParams,
): Promise<Profile[] | null> => {
return getUserSites({
handle: params.handle,
unidata: params.unidata,
})
}
export const getSite = async (input: string, customUnidata?: Unidata) => {
const profiles = await (customUnidata || unidata).profiles.get({
source: "Crossbell Profile",
identity: input,
platform: "Crossbell",
})
const site: Profile = profiles.list[0]
if (site) {
expandSite(site)
}
return site
}
export const getSites = async (input: string[]) => {
const client = createClient({
url: "https://indexer.crossbell.io/v1/graphql",
})
const result = await client
.query(
`
query getCharacters($identities: [String!], $limit: Int) {
characters( where: { handle: { in: $identities } }, orderBy: [{ updatedAt: desc }], take: $limit ) {
handle
updatedAt
characterId
notes {
updatedAt
}
metadata {
uri
content
}
}
}`,
{
identities: input,
},
)
.toPromise()
await Promise.all(
result.data?.characters?.map(async (site: any) => {
if (!site?.metadata?.content && site?.metadata?.uri) {
try {
site.metadata.content = (
await axios.get(toGateway(site?.metadata?.uri), {
...(typeof window === "undefined" && {
headers: {
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36",
},
}),
})
).data
} catch (error) {
console.warn(error)
}
}
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 || ""
site.updatedAt = [...site.notes, site]
.map((i) => i.updatedAt)
.sort()
.pop()
}),
)
result.data?.characters?.sort((a: any, b: any) => {
return b.updatedAt > a.updatedAt ? 1 : -1
})
return result.data?.characters
}
export const getSubscription = async (
siteId: string,
handle: string,
customUnidata?: Unidata,
) => {
const links = await (customUnidata || unidata).links.get({
source: "Crossbell Link",
identity: handle,
platform: "Crossbell",
filter: { to: siteId },
})
return !!links?.list?.length
}
export const getSiteSubscriptions = async (
data: {
siteId: string
cursor?: string
limit?: number
},
customUnidata?: Unidata,
) => {
const links = await (customUnidata || unidata).links.get({
source: "Crossbell Link",
identity: data.siteId,
platform: "Crossbell",
reversed: true,
cursor: data.cursor,
limit: data.limit,
})
links?.list.map(async (item: any) => {
item.character = item.metadata.from_raw
}) || []
return links
}
export const getSiteToSubscriptions = async (
data: {
siteId: string
cursor?: string
},
customUnidata?: Unidata,
) => {
const links = await (customUnidata || unidata).links.get({
source: "Crossbell Link",
identity: data.siteId,
platform: "Crossbell",
cursor: data.cursor,
})
links?.list.map(async (item: any) => {
item.character = item.metadata.to_raw
}) || []
return links
}
export async function updateSite(
payload: {
site: string
name?: string
description?: string
icon?: string | null
subdomain?: string
navigation?: SiteNavigationItem[]
css?: string
ga?: string
custom_domain?: string
banner?: {
address: string
mime_type: string
}
connected_accounts?: Profile["connected_accounts"]
},
customUnidata?: Unidata,
newbieToken?: string,
) {
return await (customUnidata || unidata).profiles.set(
{
source: "Crossbell Profile",
identity: payload.site,
platform: "Crossbell",
action: "update",
},
{
...(payload.name && { name: payload.name }),
...(payload.description && { bio: payload.description }),
...(payload.icon && { avatars: [payload.icon] }),
...(payload.banner && { banners: [payload.banner] }),
...(payload.subdomain && { username: payload.subdomain }),
...(payload.connected_accounts && {
connected_accounts: payload.connected_accounts,
}),
...((payload.navigation !== undefined ||
payload.css !== undefined ||
payload.ga !== undefined ||
payload.custom_domain !== undefined) && {
attributes: [
...(payload.navigation !== undefined
? [
{
trait_type: "xlog_navigation",
value: JSON.stringify(payload.navigation),
},
]
: []),
...(payload.css !== undefined
? [
{
trait_type: "xlog_css",
value: payload.css,
},
]
: []),
...(payload.ga !== undefined
? [
{
trait_type: "xlog_ga",
value: payload.ga,
},
]
: []),
...(payload.custom_domain !== undefined
? [
{
trait_type: "xlog_custom_domain",
value: payload.custom_domain,
},
]
: []),
],
}),
},
{
newbieToken,
},
)
}
export async function createSite(
address: string,
payload: { name: string; subdomain: string },
customUnidata?: Unidata,
) {
return await (customUnidata || unidata).profiles.set(
{
source: "Crossbell Profile",
identity: address,
platform: "Ethereum",
action: "add",
},
{
username: payload.subdomain,
name: payload.name,
tags: [
"navigation:" +
JSON.stringify([
{
id: nanoid(),
label: "Archives",
url: "/archives",
},
]),
],
},
)
}
export async function subscribeToSites(
input: {
user: Profile
sites: {
characterId: string
}[]
},
contract?: Contract,
) {
if (input.user.metadata?.proof) {
return contract?.linkCharactersInBatch(
input.user.metadata.proof,
input.sites.map((s) => s.characterId).filter((c) => c) as any,
[],
"follow",
)
}
}
const xLogOperatorPermissions: CharacterOperatorPermission[] = [
CharacterOperatorPermission.SET_NOTE_URI,
CharacterOperatorPermission.DELETE_NOTE,
CharacterOperatorPermission.POST_NOTE,
CharacterOperatorPermission.SET_CHARACTER_URI,
]
export async function addOperator(
input: {
characterId: number
operator: string
},
contract?: Contract,
) {
if (input.operator && input.characterId) {
return contract?.grantOperatorPermissionsForCharacter(
input.characterId,
input.operator,
xLogOperatorPermissions,
)
}
}
export async function getOperators(input: { characterId?: number }) {
if (input.characterId) {
const result = await indexer?.getCharacterOperators(input.characterId, {
limit: 100,
})
result.list = result.list
.filter(
(o) =>
o.operator !== "0x0000000000000000000000000000000000000000" &&
o.operator !== "0x0f588318a494e4508a121a32b6670b5494ca3357" &&
o.operator !== "0xbbc2918c9003d264c25ecae45b44a846702c0e7c",
) // remove 0 and xSync
.filter((o) => {
for (const permission of xLogOperatorPermissions) {
if (!o.permissions.includes(permission)) {
return false
}
}
return true
})
return result
}
}
export async function isOperators(input: {
characterId: number
operator: string
}) {
if (input.characterId) {
const permissions =
(await indexer?.getCharacterOperator(input.characterId, input.operator))
?.permissions || []
for (const permission of xLogOperatorPermissions) {
if (!permissions.includes(permission)) {
return false
}
}
return true
}
return false
}
export async function removeOperator(
input: {
characterId: number
operator: string
},
contract?: Contract,
) {
if (input.characterId) {
return contract?.grantOperatorPermissionsForCharacter(
input.characterId,
input.operator,
[],
)
}
}
export async function getNFTs(address: string, customUnidata?: Unidata) {
const assets = await (customUnidata || unidata).assets.get({
source: "Ethereum NFT",
identity: address,
})
return assets
}
export async function getStat({ characterId }: { characterId: string }) {
if (characterId) {
const [stat, site, subscriptions, comments, notes] = await Promise.all([
(
await fetch(
`https://indexer.crossbell.io/v1/stat/characters/${characterId}`,
)
).json(),
indexer.getCharacter(characterId),
indexer.getBacklinksOfCharacter(characterId, {
limit: 0,
}),
indexer.getNotes({
limit: 0,
toCharacterId: characterId,
}),
indexer.getNotes({
characterId,
sources: "xlog",
tags: ["post"],
limit: 0,
}),
])
return {
viewsCount: stat.viewNoteCount,
createdAt: site?.createdAt,
subscriptionCount: subscriptions?.count,
commentsCount: comments?.count,
notesCount: notes?.count,
}
}
}