import { database } from '$lib/Database/IDB/activities'; import type { User } from './follow'; import type { AniListAuthorisation, UserIdentity } from './identity'; export interface ActivityHistoryEntry { date: number; amount: number; } interface ActivityHistoryOptions { stats: { activityHistory: ActivityHistoryEntry[]; }; options: { timezone: string; }; } interface LastActivity { date: Date; timezone: string; } export const fillMissingDays = ( inputActivities: ActivityHistoryEntry[], startOfYear = false, year = new Date().getFullYear() ): ActivityHistoryEntry[] => { const yearDate = new Date(year, 0, 0); if (inputActivities.length === 0) return startOfYear ? fillDateRange( new Date(yearDate.getUTCFullYear(), 0, 1), new Date(yearDate.getUTCFullYear() + 1, 0, 1) ) : []; const sortedActivities = [...inputActivities].sort((a, b) => a.date - b.date); const endDate = new Date(sortedActivities[sortedActivities.length - 1].date * 1000); endDate.setUTCDate(endDate.getUTCDate() + 1); return fillDateRange( startOfYear ? new Date(yearDate.getUTCFullYear(), 0, 1) : new Date(sortedActivities[0].date * 1000), endDate, sortedActivities ); }; const fillDateRange = ( startDate: Date, endDate: Date, existingActivities: ActivityHistoryEntry[] = [] ): ActivityHistoryEntry[] => { const outputActivities: ActivityHistoryEntry[] = []; for (let dt = new Date(startDate); dt < endDate; dt.setUTCDate(dt.getUTCDate() + 1)) { const dateString = dt.toDateString(); if ( !new Set( existingActivities.map((activity) => new Date(activity.date * 1000).toDateString()) ).has(dateString) ) { outputActivities.push({ date: Math.floor(dt.getTime() / 1000), amount: 0 }); } else { const activity = existingActivities.find( (activity) => new Date(activity.date * 1000).toDateString() === dateString ); if (activity) outputActivities.push(activity); } } return outputActivities; }; export const activityHistoryOptions = async ( userIdentity: UserIdentity, authorisation?: AniListAuthorisation ): Promise => { return ( await ( await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(authorisation ? { Authorization: `${authorisation.tokenType} ${authorisation.accessToken}` } : {}) }, body: JSON.stringify({ query: `{ User(id: ${userIdentity.id}) { stats { activityHistory { date amount } } ${authorisation ? 'options { timezone }' : ''} } }` }) }) ).json() )['data']['User']; }; const convertToTimezoneOffset = (timeStr: string) => { const [hours, minutes] = timeStr.split(':'); let totalMinutes = parseInt(hours, 10) * 60 + parseInt(minutes, 10); totalMinutes = -totalMinutes; return totalMinutes; }; export const activityHistory = async ( userIdentity: UserIdentity, authorisation?: AniListAuthorisation ): Promise => (await activityHistoryOptions(userIdentity, authorisation)).stats.activityHistory; export const lastActivityDate = async ( userIdentity: UserIdentity, authorisation: AniListAuthorisation ): Promise => { if (userIdentity.id === -1 || userIdentity.id === -2) return { date: new Date(8640000000000000), timezone: '' }; const history = await activityHistoryOptions(userIdentity, authorisation); const date = new Date( Number(history.stats.activityHistory[history.stats.activityHistory.length - 1].date) * 1000 + convertToTimezoneOffset(history.options.timezone) ); date.setDate(date.getDate() + 1); return { date, timezone: history.options.timezone }; }; export interface ActivitiesPage { data: { Page: { pageInfo: { hasNextPage: boolean; }; activities: { createdAt: number; }[]; }; }; } const activitiesPage = async ( page: number, anilistAuthorisation: AniListAuthorisation, userIdentity: UserIdentity, year = new Date().getFullYear() ): Promise => await ( await fetch('https://graphql.anilist.co', { method: 'POST', headers: { Authorization: `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: `{ Page(page: ${page}) { pageInfo { hasNextPage } activities(userId: ${userIdentity.id}, createdAt_greater: ${Math.floor( new Date(year, 0, 1).getTime() / 1000 )}, createdAt_lesser: ${Math.floor(new Date(year, 7, 1).getTime() / 1000)}) { ... on TextActivity { createdAt } ... on ListActivity { createdAt } ... on MessageActivity { createdAt } } } }` }) }) ).json(); export const fullActivityHistory = async ( anilistAuthorisation: AniListAuthorisation, userIdentity: UserIdentity, year = new Date().getFullYear(), disableLoopingActivityCounter = false ): Promise => { const activities = []; let page = 1; let currentDatabasePage = await database.activities.get(page); let currentPage; if (currentDatabasePage) currentPage = currentDatabasePage.data; else { currentPage = await activitiesPage(page, anilistAuthorisation, userIdentity, year); database.activities.add({ page, data: currentPage }); } for (const activity of currentPage.data.Page.activities) activities.push(activity); while (currentPage['data']['Page']['pageInfo']['hasNextPage']) { if (disableLoopingActivityCounter) break; for (const activity of currentPage.data.Page.activities) activities.push(activity); page += 1; currentDatabasePage = await database.activities.get(page); if (currentDatabasePage) currentPage = currentDatabasePage.data; else { currentPage = await activitiesPage(page, anilistAuthorisation, userIdentity, year); database.activities.add({ page, data: currentPage }); } } for (const activity of currentPage.data.Page.activities) activities.push(activity); let fullLocalActivityHistory: ActivityHistoryEntry[] = []; for (const activity of activities) { const date = new Date(activity.createdAt * 1000); const dateString = date.toDateString(); const activityHistoryEntry = fullLocalActivityHistory.find( (activityHistoryEntry) => new Date(activityHistoryEntry.date * 1000).toDateString() === dateString ); if (activityHistoryEntry) activityHistoryEntry.amount += 1; else fullLocalActivityHistory.push({ date: Math.floor(date.getTime() / 1000), amount: 1 }); } fullLocalActivityHistory = fullLocalActivityHistory.filter((a) => !isNaN(a.date)); if (new Date().getMonth() > 6) fullLocalActivityHistory.push(...(await activityHistory(userIdentity))); fullLocalActivityHistory = fullLocalActivityHistory.filter( (activityHistoryEntry, index, self) => self.findIndex( (a) => new Date(a.date * 1000).toDateString() === new Date(activityHistoryEntry.date * 1000).toDateString() ) === index ); fullLocalActivityHistory = fullLocalActivityHistory.filter( (activityHistoryEntry) => new Date(activityHistoryEntry.date * 1000).getFullYear() === year ); return fullLocalActivityHistory; }; export const activityLikes = async (id: number): Promise[]> => { const activityResponse = await ( await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: `{ Activity(id: ${id}) { __typename ... on TextActivity { likes { name avatar { large } } } ... on ListActivity { likes { name avatar { large } } } ... on MessageActivity { likes { name avatar { large } } } } }` }) }) ).json(); return activityResponse['data']['Activity']['likes']; }; export const activityText = async (id: number, replies = false): Promise => { const activityResponse = await ( await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query: `{ Activity(id: ${id}) { ... on TextActivity { text(asHtml: true) ${replies ? 'replies { text(asHtml: true) }' : ''} } } }` }) }) ).json(); let text = activityResponse['data']['Activity']['text']; if (replies) for (const reply of activityResponse['data']['Activity']['replies']) text += reply.text; return text; };