Instance Ticker
This commit is contained in:
		
							parent
							
								
									25bd82ecaa
								
							
						
					
					
						commit
						615fedd64d
					
				
					 14 changed files with 280 additions and 25 deletions
				
			
		|  | @ -597,6 +597,12 @@ openInNewTab: "新しいタブで開く" | ||||||
| openInSideView: "サイドビューで開く" | openInSideView: "サイドビューで開く" | ||||||
| defaultNavigationBehaviour: "デフォルトのナビゲーション" | defaultNavigationBehaviour: "デフォルトのナビゲーション" | ||||||
| editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。" | editTheseSettingsMayBreakAccount: "これらの設定を編集するとアカウントが破損する可能性があります。" | ||||||
|  | instanceTicker: "ノートのインスタンス情報" | ||||||
|  | 
 | ||||||
|  | _instanceTicker: | ||||||
|  |   none: "表示しない" | ||||||
|  |   remote: "リモートユーザーに表示" | ||||||
|  |   always: "常に表示" | ||||||
| 
 | 
 | ||||||
| _serverDisconnectedBehavior: | _serverDisconnectedBehavior: | ||||||
|   reload: "自動でリロード" |   reload: "自動でリロード" | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								migration/1603776877564-instance-theme-color.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1603776877564-instance-theme-color.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class instanceThemeColor1603776877564 implements MigrationInterface { | ||||||
|  |     name = 'instanceThemeColor1603776877564' | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "instance" ADD "themeColor" character varying(64) DEFAULT null`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "themeColor"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								migration/1603781553011-instance-favicon.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1603781553011-instance-favicon.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class instanceFavicon1603781553011 implements MigrationInterface { | ||||||
|  |     name = 'instanceFavicon1603781553011' | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "instance" ADD "faviconUrl" character varying(256) DEFAULT null`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "faviconUrl"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										61
									
								
								src/client/components/instance-ticker.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/client/components/instance-ticker.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | ||||||
|  | <template> | ||||||
|  | <div class="hpaizdrt" :style="bg"> | ||||||
|  | 	<img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/> | ||||||
|  | 	<span class="name">{{ info.name }}</span> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import { defineComponent } from 'vue'; | ||||||
|  | import { instanceName } from '@/config'; | ||||||
|  | 
 | ||||||
|  | export default defineComponent({ | ||||||
|  | 	props: { | ||||||
|  | 		instance: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			info: this.instance || { | ||||||
|  | 				faviconUrl: '/favicon.ico', | ||||||
|  | 				name: instanceName, | ||||||
|  | 				themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	computed: { | ||||||
|  | 		bg(): any { | ||||||
|  | 			return this.info.themeColor ? { | ||||||
|  | 				background: `linear-gradient(90deg, ${this.info.themeColor}, ${this.info.themeColor + '00'})` | ||||||
|  | 			} : null; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .hpaizdrt { | ||||||
|  | 	$height: 1.1rem; | ||||||
|  | 
 | ||||||
|  | 	height: $height; | ||||||
|  | 	border-radius: 4px 0 0 4px; | ||||||
|  | 	overflow: hidden; | ||||||
|  | 	color: #fff; | ||||||
|  | 
 | ||||||
|  | 	> .icon { | ||||||
|  | 		height: 100%; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	> .name { | ||||||
|  | 		margin-left: 4px; | ||||||
|  | 		line-height: $height; | ||||||
|  | 		font-size: 0.9em; | ||||||
|  | 		vertical-align: top; | ||||||
|  | 		font-weight: bold; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | @ -40,6 +40,7 @@ | ||||||
| 		<MkAvatar class="avatar" :user="appearNote.user"/> | 		<MkAvatar class="avatar" :user="appearNote.user"/> | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<XNoteHeader class="header" :note="appearNote" :mini="true"/> | 			<XNoteHeader class="header" :note="appearNote" :mini="true"/> | ||||||
|  | 			<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/> | ||||||
| 			<div class="body" ref="noteBody"> | 			<div class="body" ref="noteBody"> | ||||||
| 				<p v-if="appearNote.cw != null" class="cw"> | 				<p v-if="appearNote.cw != null" class="cw"> | ||||||
| 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> | 					<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/> | ||||||
|  | @ -139,6 +140,7 @@ export default defineComponent({ | ||||||
| 		XCwButton, | 		XCwButton, | ||||||
| 		XPoll, | 		XPoll, | ||||||
| 		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), | 		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), | ||||||
|  | 		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	inject: { | 	inject: { | ||||||
|  | @ -258,6 +260,12 @@ export default defineComponent({ | ||||||
| 			} else { | 			} else { | ||||||
| 				return null; | 				return null; | ||||||
| 			} | 			} | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		showTicker() { | ||||||
|  | 			if (this.$store.state.device.instanceTicker === 'always') return true; | ||||||
|  | 			if (this.$store.state.device.instanceTicker === 'remote' && this.appearNote.user.instance) return true; | ||||||
|  | 			return false; | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -246,7 +246,7 @@ export default defineComponent({ | ||||||
| 				icon: faQuestionCircle, | 				icon: faQuestionCircle, | ||||||
| 			}, { | 			}, { | ||||||
| 				type: 'link', | 				type: 'link', | ||||||
| 				text: this.$t('aboutX', { x: instanceName || host }), | 				text: this.$t('aboutX', { x: instanceName }), | ||||||
| 				to: '/about', | 				to: '/about', | ||||||
| 				icon: faInfoCircle, | 				icon: faInfoCircle, | ||||||
| 			}, { | 			}, { | ||||||
|  |  | ||||||
|  | @ -12,5 +12,5 @@ export const lang = localStorage.getItem('lang'); | ||||||
| export const langs = _LANGS_; | export const langs = _LANGS_; | ||||||
| export const getLocale = async () => Object.fromEntries((await entries(clientDb.i18n)) as [string, string][]); | export const getLocale = async () => Object.fromEntries((await entries(clientDb.i18n)) as [string, string][]); | ||||||
| export const version = _VERSION_; | export const version = _VERSION_; | ||||||
| export const instanceName = siteName === 'Misskey' ? null : siteName; | export const instanceName = siteName === 'Misskey' ? host : siteName; | ||||||
| export const deckmode = localStorage.getItem('deckmode') === 'true'; | export const deckmode = localStorage.getItem('deckmode') === 'true'; | ||||||
|  |  | ||||||
|  | @ -51,6 +51,12 @@ | ||||||
| 			<MkRadio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></MkRadio> | 			<MkRadio v-model="fontSize" value="large"><span style="font-size: 18px;">Aa</span></MkRadio> | ||||||
| 			<MkRadio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></MkRadio> | 			<MkRadio v-model="fontSize" value="veryLarge"><span style="font-size: 20px;">Aa</span></MkRadio> | ||||||
| 		</div> | 		</div> | ||||||
|  | 		<div class="_content"> | ||||||
|  | 			<div>{{ $t('instanceTicker') }}</div> | ||||||
|  | 			<MkRadio v-model="instanceTicker" value="none">{{ $t('_instanceTicker.none') }}</MkRadio> | ||||||
|  | 			<MkRadio v-model="instanceTicker" value="remote">{{ $t('_instanceTicker.remote') }}</MkRadio> | ||||||
|  | 			<MkRadio v-model="instanceTicker" value="always">{{ $t('_instanceTicker.always') }}</MkRadio> | ||||||
|  | 		</div> | ||||||
| 	</section> | 	</section> | ||||||
| 
 | 
 | ||||||
| 	<section class="_card _vMargin"> | 	<section class="_card _vMargin"> | ||||||
|  | @ -169,6 +175,11 @@ export default defineComponent({ | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); } | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		instanceTicker: { | ||||||
|  | 			get() { return this.$store.state.device.instanceTicker; }, | ||||||
|  | 			set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); } | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		enableInfiniteScroll: { | 		enableInfiniteScroll: { | ||||||
| 			get() { return this.$store.state.device.enableInfiniteScroll; }, | 			get() { return this.$store.state.device.enableInfiniteScroll; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); } | ||||||
|  |  | ||||||
|  | @ -77,6 +77,7 @@ export const defaultDeviceSettings = { | ||||||
| 	enableInfiniteScroll: true, | 	enableInfiniteScroll: true, | ||||||
| 	useBlurEffectForModal: true, | 	useBlurEffectForModal: true, | ||||||
| 	sidebarDisplay: 'full', // full, icon, hide
 | 	sidebarDisplay: 'full', // full, icon, hide
 | ||||||
|  | 	instanceTicker: 'remote', // none, remote, always
 | ||||||
| 	roomGraphicsQuality: 'medium', | 	roomGraphicsQuality: 'medium', | ||||||
| 	roomUseOrthographicCamera: true, | 	roomUseOrthographicCamera: true, | ||||||
| 	deckColumnAlign: 'left', | 	deckColumnAlign: 'left', | ||||||
|  |  | ||||||
|  | @ -4,11 +4,11 @@ | ||||||
| 		<MkA class="link" to="/">{{ $t('home') }}</MkA> | 		<MkA class="link" to="/">{{ $t('home') }}</MkA> | ||||||
| 		<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA> | 		<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA> | ||||||
| 		<MkA class="link" to="/channels">{{ $t('channel') }}</MkA> | 		<MkA class="link" to="/channels">{{ $t('channel') }}</MkA> | ||||||
| 		<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName || host }) }}</MkA> | 		<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA> | ||||||
| 	</header> | 	</header> | ||||||
| 
 | 
 | ||||||
| 	<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }"> | 	<div class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }"> | ||||||
| 		<h1>{{ instanceName || host }}</h1> | 		<h1>{{ instanceName }}</h1> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="contents" ref="contents" :class="{ wallpaper }"> | 	<div class="contents" ref="contents" :class="{ wallpaper }"> | ||||||
|  |  | ||||||
|  | @ -163,6 +163,16 @@ export class Instance { | ||||||
| 	}) | 	}) | ||||||
| 	public iconUrl: string | null; | 	public iconUrl: string | null; | ||||||
| 
 | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 256, nullable: true, default: null, | ||||||
|  | 	}) | ||||||
|  | 	public faviconUrl: string | null; | ||||||
|  | 
 | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 64, nullable: true, default: null, | ||||||
|  | 	}) | ||||||
|  | 	public themeColor: string | null; | ||||||
|  | 
 | ||||||
| 	@Column('timestamp with time zone', { | 	@Column('timestamp with time zone', { | ||||||
| 		nullable: true, | 		nullable: true, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import { EntityRepository, Repository, In, Not } from 'typeorm'; | import { EntityRepository, Repository, In, Not } from 'typeorm'; | ||||||
| import { User, ILocalUser, IRemoteUser } from '../entities/user'; | import { User, ILocalUser, IRemoteUser } from '../entities/user'; | ||||||
| import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings } from '..'; | import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..'; | ||||||
| import { ensure } from '../../prelude/ensure'; | import { ensure } from '../../prelude/ensure'; | ||||||
| import config from '../../config'; | import config from '../../config'; | ||||||
| import { SchemaType } from '../../misc/schema'; | import { SchemaType } from '../../misc/schema'; | ||||||
|  | @ -181,6 +181,14 @@ export class UserRepository extends Repository<User> { | ||||||
| 			isModerator: user.isModerator || falsy, | 			isModerator: user.isModerator || falsy, | ||||||
| 			isBot: user.isBot || falsy, | 			isBot: user.isBot || falsy, | ||||||
| 			isCat: user.isCat || falsy, | 			isCat: user.isCat || falsy, | ||||||
|  | 			instance: user.host ? Instances.findOne({ host: user.host }).then(instance => instance ? { | ||||||
|  | 				name: instance.name, | ||||||
|  | 				softwareName: instance.softwareName, | ||||||
|  | 				softwareVersion: instance.softwareVersion, | ||||||
|  | 				iconUrl: instance.iconUrl, | ||||||
|  | 				faviconUrl: instance.faviconUrl, | ||||||
|  | 				themeColor: instance.themeColor, | ||||||
|  | 			} : undefined) : undefined, | ||||||
| 
 | 
 | ||||||
| 			// カスタム絵文字添付
 | 			// カスタム絵文字添付
 | ||||||
| 			emojis: user.emojis.length > 0 ? Emojis.find({ | 			emojis: user.emojis.length > 0 ? Emojis.find({ | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ html | ||||||
| 		meta(name='application-name' content='Misskey') | 		meta(name='application-name' content='Misskey') | ||||||
| 		meta(name='referrer' content='origin') | 		meta(name='referrer' content='origin') | ||||||
| 		meta(name='theme-color' content='#86b300') | 		meta(name='theme-color' content='#86b300') | ||||||
|  | 		meta(name='theme-color-orig' content='#86b300') | ||||||
| 		meta(property='og:site_name' content= instanceName || 'Misskey') | 		meta(property='og:site_name' content= instanceName || 'Misskey') | ||||||
| 		meta(name='viewport' content='width=device-width, initial-scale=1') | 		meta(name='viewport' content='width=device-width, initial-scale=1') | ||||||
| 		link(rel='icon' href= icon || '/favicon.ico') | 		link(rel='icon' href= icon || '/favicon.ico') | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { JSDOM } from 'jsdom'; | import { DOMWindow, JSDOM } from 'jsdom'; | ||||||
| import fetch from 'node-fetch'; | import fetch from 'node-fetch'; | ||||||
| import { getJson, getHtml, getAgentByUrl } from '../misc/fetch'; | import { getJson, getHtml, getAgentByUrl } from '../misc/fetch'; | ||||||
| import { Instance } from '../models/entities/instance'; | import { Instance } from '../models/entities/instance'; | ||||||
|  | @ -22,9 +22,18 @@ export async function fetchInstanceMetadata(instance: Instance): Promise<void> { | ||||||
| 	logger.info(`Fetching metadata of ${instance.host} ...`); | 	logger.info(`Fetching metadata of ${instance.host} ...`); | ||||||
| 
 | 
 | ||||||
| 	try { | 	try { | ||||||
| 		const [info, icon] = await Promise.all([ | 		const [info, dom, manifest] = await Promise.all([ | ||||||
| 			fetchNodeinfo(instance).catch(() => null), | 			fetchNodeinfo(instance).catch(() => null), | ||||||
| 			fetchIconUrl(instance).catch(() => null), | 			fetchDom(instance).catch(() => null), | ||||||
|  | 			fetchManifest(instance).catch(() => null), | ||||||
|  | 		]); | ||||||
|  | 
 | ||||||
|  | 		const [favicon, icon, themeColor, name, description] = await Promise.all([ | ||||||
|  | 			fetchFaviconUrl(instance).catch(() => null), | ||||||
|  | 			fetchIconUrl(instance, dom, manifest).catch(() => null), | ||||||
|  | 			getThemeColor(dom, manifest).catch(() => null), | ||||||
|  | 			getSiteName(info, dom, manifest).catch(() => null), | ||||||
|  | 			getDescription(info, dom, manifest).catch(() => null), | ||||||
| 		]); | 		]); | ||||||
| 
 | 
 | ||||||
| 		logger.succ(`Successfuly fetched metadata of ${instance.host}`); | 		logger.succ(`Successfuly fetched metadata of ${instance.host}`); | ||||||
|  | @ -34,18 +43,18 @@ export async function fetchInstanceMetadata(instance: Instance): Promise<void> { | ||||||
| 		} as Record<string, any>; | 		} as Record<string, any>; | ||||||
| 
 | 
 | ||||||
| 		if (info) { | 		if (info) { | ||||||
| 			updates.softwareName = info.software.name.toLowerCase(); | 			updates.softwareName = info.software?.name.toLowerCase(); | ||||||
| 			updates.softwareVersion = info.software.version; | 			updates.softwareVersion = info.software?.version; | ||||||
| 			updates.openRegistrations = info.openRegistrations; | 			updates.openRegistrations = info.openRegistrations; | ||||||
| 			updates.name = info.metadata ? (info.metadata.nodeName || info.metadata.name || null) : null; |  | ||||||
| 			updates.description = info.metadata ? (info.metadata.nodeDescription || info.metadata.description || null) : null; |  | ||||||
| 			updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null; | 			updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name || null) : null : null; | ||||||
| 			updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null; | 			updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email || null) : null : null; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (icon) { | 		if (name) updates.name = name; | ||||||
| 			updates.iconUrl = icon; | 		if (description) updates.description = description; | ||||||
| 		} | 		if (icon || favicon) updates.iconUrl = icon || favicon; | ||||||
|  | 		if (favicon) updates.faviconUrl = favicon; | ||||||
|  | 		if (themeColor) updates.themeColor = themeColor; | ||||||
| 
 | 
 | ||||||
| 		await Instances.update(instance.id, updates); | 		await Instances.update(instance.id, updates); | ||||||
| 
 | 
 | ||||||
|  | @ -57,7 +66,25 @@ export async function fetchInstanceMetadata(instance: Instance): Promise<void> { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> { | type NodeInfo = { | ||||||
|  | 	openRegistrations?: any; | ||||||
|  | 	software?: { | ||||||
|  | 		name?: any; | ||||||
|  | 		version?: any; | ||||||
|  | 	}; | ||||||
|  | 	metadata?: { | ||||||
|  | 		name?: any; | ||||||
|  | 		nodeName?: any; | ||||||
|  | 		nodeDescription?: any; | ||||||
|  | 		description?: any; | ||||||
|  | 		maintainer?: { | ||||||
|  | 			name?: any; | ||||||
|  | 			email?: any; | ||||||
|  | 		}; | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | async function fetchNodeinfo(instance: Instance): Promise<NodeInfo> { | ||||||
| 	logger.info(`Fetching nodeinfo of ${instance.host} ...`); | 	logger.info(`Fetching nodeinfo of ${instance.host} ...`); | ||||||
| 
 | 
 | ||||||
| 	try { | 	try { | ||||||
|  | @ -100,8 +127,8 @@ async function fetchNodeinfo(instance: Instance): Promise<Record<string, any>> { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function fetchIconUrl(instance: Instance): Promise<string | null> { | async function fetchDom(instance: Instance): Promise<DOMWindow['document']> { | ||||||
| 	logger.info(`Fetching icon URL of ${instance.host} ...`); | 	logger.info(`Fetching HTML of ${instance.host} ...`); | ||||||
| 
 | 
 | ||||||
| 	const url = 'https://' + instance.host; | 	const url = 'https://' + instance.host; | ||||||
| 
 | 
 | ||||||
|  | @ -110,16 +137,23 @@ async function fetchIconUrl(instance: Instance): Promise<string | null> { | ||||||
| 	const { window } = new JSDOM(html); | 	const { window } = new JSDOM(html); | ||||||
| 	const doc = window.document; | 	const doc = window.document; | ||||||
| 
 | 
 | ||||||
| 	const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href'); | 	return doc; | ||||||
| 	const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); | } | ||||||
| 	const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href'); |  | ||||||
| 
 | 
 | ||||||
| 	const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon; | async function fetchManifest(instance: Instance): Promise<Record<string, any> | null> { | ||||||
|  | 	const url = 'https://' + instance.host; | ||||||
| 
 | 
 | ||||||
| 	if (href) { | 	const manifestUrl = url + '/manifest.json'; | ||||||
| 		return (new URL(href, url)).href; |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
|  | 	const manifest = await getJson(manifestUrl); | ||||||
|  | 
 | ||||||
|  | 	return manifest; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function fetchFaviconUrl(instance: Instance): Promise<string | null> { | ||||||
|  | 	logger.info(`Fetching favicon URL of ${instance.host} ...`); | ||||||
|  | 
 | ||||||
|  | 	const url = 'https://' + instance.host; | ||||||
| 	const faviconUrl = url + '/favicon.ico'; | 	const faviconUrl = url + '/favicon.ico'; | ||||||
| 
 | 
 | ||||||
| 	const favicon = await fetch(faviconUrl, { | 	const favicon = await fetch(faviconUrl, { | ||||||
|  | @ -133,3 +167,90 @@ async function fetchIconUrl(instance: Instance): Promise<string | null> { | ||||||
| 
 | 
 | ||||||
| 	return null; | 	return null; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { | ||||||
|  | 	if (doc) { | ||||||
|  | 		const url = 'https://' + instance.host; | ||||||
|  | 
 | ||||||
|  | 		const hrefAppleTouchIconPrecomposed = doc.querySelector('link[rel="apple-touch-icon-precomposed"]')?.getAttribute('href'); | ||||||
|  | 		const hrefAppleTouchIcon = doc.querySelector('link[rel="apple-touch-icon"]')?.getAttribute('href'); | ||||||
|  | 		const hrefIcon = doc.querySelector('link[rel="icon"]')?.getAttribute('href'); | ||||||
|  | 
 | ||||||
|  | 		const href = hrefAppleTouchIconPrecomposed || hrefAppleTouchIcon || hrefIcon; | ||||||
|  | 
 | ||||||
|  | 		if (href) { | ||||||
|  | 			return (new URL(href, url)).href; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { | ||||||
|  | 		const url = 'https://' + instance.host; | ||||||
|  | 		return (new URL(manifest.icons[0].src, url)).href; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { | ||||||
|  | 	if (doc) { | ||||||
|  | 		const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content'); | ||||||
|  | 
 | ||||||
|  | 		if (themeColor) { | ||||||
|  | 			return themeColor; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (manifest) { | ||||||
|  | 		return manifest.theme_color; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function getSiteName(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { | ||||||
|  | 	if (info && info.metadata) { | ||||||
|  | 		if (info.metadata.nodeName || info.metadata.name) { | ||||||
|  | 			return info.metadata.nodeName || info.metadata.name; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (doc) { | ||||||
|  | 		const og = doc.querySelector('meta[property="og:title"]')?.getAttribute('content'); | ||||||
|  | 
 | ||||||
|  | 		if (og) { | ||||||
|  | 			return og; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (manifest) { | ||||||
|  | 		return manifest?.name || manifest?.short_name; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function getDescription(info: NodeInfo | null, doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> { | ||||||
|  | 	if (info && info.metadata) { | ||||||
|  | 		if (info.metadata.nodeDescription || info.metadata.description) { | ||||||
|  | 			return info.metadata.nodeDescription || info.metadata.description; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (doc) { | ||||||
|  | 		const meta = doc.querySelector('meta[name="description"]')?.getAttribute('content'); | ||||||
|  | 		if (meta) { | ||||||
|  | 			return meta; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const og = doc.querySelector('meta[property="og:description"]')?.getAttribute('content'); | ||||||
|  | 		if (og) { | ||||||
|  | 			return og; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if (manifest) { | ||||||
|  | 		return manifest?.name || manifest?.short_name; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return null; | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue