Compare commits
2 Commits
a08f1a990a
...
a8ea81015b
Author | SHA1 | Date |
---|---|---|
Derek | a8ea81015b | |
Derek | dc31a8f16c |
|
@ -0,0 +1,14 @@
|
||||||
|
export type Grp = {
|
||||||
|
name: string;
|
||||||
|
host: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parse(grp: string): Grp {
|
||||||
|
if (grp.startsWith('+')) grp = grp.substr(1);
|
||||||
|
const split = grp.split('@', 2);
|
||||||
|
return { name: split[0], host: split[1] || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toString(grp: Grp): string {
|
||||||
|
return grp.host == null ? grp.name : `${grp.name}@${grp.host}`;
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { URL } from 'node:url';
|
||||||
|
import webFinger from './webfinger.js';
|
||||||
|
import config from '@/config/index.js';
|
||||||
|
import { createGroup, updateGroup } from './activitypub/models/group.js';
|
||||||
|
import { remoteLogger } from './logger.js';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { Channel } from '@/models/entities/channel.js';
|
||||||
|
import { Channels } from '@/models/index.js';
|
||||||
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
|
||||||
|
const logger = remoteLogger.createSubLogger('resolve');
|
||||||
|
|
||||||
|
export async function resolveChannel(channelName: string, host: string | null): Promise<Channel> {
|
||||||
|
const nameLower = channelName.toLowerCase();
|
||||||
|
|
||||||
|
if (host == null) {
|
||||||
|
logger.info(`return local channel: ${nameLower}`);
|
||||||
|
return await Channels.findOneBy({ nameLower, host: IsNull() }).then(u => {
|
||||||
|
if (u == null) {
|
||||||
|
throw new Error('channel not found');
|
||||||
|
} else {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
host = toPuny(host);
|
||||||
|
|
||||||
|
if (config.host === host) {
|
||||||
|
logger.info(`return local channel: ${nameLower}`);
|
||||||
|
return await Channels.findOneBy({ nameLower, host: IsNull() }).then(u => {
|
||||||
|
if (u == null) {
|
||||||
|
throw new Error('channel not found');
|
||||||
|
} else {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await Channels.findOneBy({ nameLower, host }) | null;
|
||||||
|
|
||||||
|
const identifier = `${nameLower}@${host}`;
|
||||||
|
|
||||||
|
if (channel == null) {
|
||||||
|
const self = await resolveSelf(identifier);
|
||||||
|
|
||||||
|
logger.succ(`return new remote channel: ${chalk.magenta(nameLower)}`);
|
||||||
|
return await createGroup(self.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`return existing remote user: ${nameLower}`);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSelf(nameLower: string) {
|
||||||
|
logger.info(`WebFinger for ${chalk.yellow(nameLower)}`);
|
||||||
|
const finger = await webFinger(nameLower, 'grp').catch(e => {
|
||||||
|
logger.error(`Failed to WebFinger for ${chalk.yellow(nameLower)}: ${ e.statusCode || e.message }`);
|
||||||
|
throw new Error(`Failed to WebFinger for ${nameLower}: ${ e.statusCode || e.message }`);
|
||||||
|
});
|
||||||
|
const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self');
|
||||||
|
if (!self) {
|
||||||
|
logger.error(`Failed to WebFinger for ${chalk.yellow(nameLower)}: self link not found`);
|
||||||
|
throw new Error('self link not found');
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import { Users } from '@/models/index.js';
|
||||||
import { toPuny } from '@/misc/convert-host.js';
|
import { toPuny } from '@/misc/convert-host.js';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
|
||||||
const logger = remoteLogger.createSubLogger('resolve-user');
|
const logger = remoteLogger.createSubLogger('resolve');
|
||||||
|
|
||||||
export async function resolveUser(username: string, host: string | null): Promise<User> {
|
export async function resolveUser(username: string, host: string | null): Promise<User> {
|
||||||
const usernameLower = username.toLowerCase();
|
const usernameLower = username.toLowerCase();
|
||||||
|
@ -98,7 +98,7 @@ export async function resolveUser(username: string, host: string | null): Promis
|
||||||
|
|
||||||
async function resolveSelf(acctLower: string) {
|
async function resolveSelf(acctLower: string) {
|
||||||
logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
|
logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
|
||||||
const finger = await webFinger(acctLower).catch(e => {
|
const finger = await webFinger(acctLower, 'acct').catch(e => {
|
||||||
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
|
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
|
||||||
throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
|
throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,13 +12,13 @@ type IWebFinger = {
|
||||||
subject: string;
|
subject: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function(query: string): Promise<IWebFinger> {
|
export default async function(query: string, schema: string): Promise<IWebFinger> {
|
||||||
const url = genUrl(query);
|
const url = genUrl(query, schema);
|
||||||
|
|
||||||
return await getJson(url, 'application/jrd+json, application/json') as IWebFinger;
|
return await getJson(url, 'application/jrd+json, application/json') as IWebFinger;
|
||||||
}
|
}
|
||||||
|
|
||||||
function genUrl(query: string) {
|
function genUrl(query: string, schema: string) {
|
||||||
if (query.match(/^https?:\/\//)) {
|
if (query.match(/^https?:\/\//)) {
|
||||||
const u = new URL(query);
|
const u = new URL(query);
|
||||||
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
||||||
|
@ -27,7 +27,7 @@ function genUrl(query: string) {
|
||||||
const m = query.match(/^([^@]+)@(.*)/);
|
const m = query.match(/^([^@]+)@(.*)/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const hostname = m[2];
|
const hostname = m[2];
|
||||||
return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
|
return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `${schema}:${query}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Invalid query (${query})`);
|
throw new Error(`Invalid query (${query})`);
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { Channels } from '@/models/index.js';
|
import { Channels } from '@/models/index.js';
|
||||||
|
import { Channel } from '@/models/entities/channel.js';
|
||||||
|
import { FindOptionsWhere, In, IsNull } from 'typeorm';
|
||||||
|
import { resolveChannel } from '@/remote/resolve-channel.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['channels'],
|
tags: ['channels'],
|
||||||
|
@ -14,6 +17,13 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
failedToResolveRemoteChannel: {
|
||||||
|
message: 'Failed to resolve remote channel.',
|
||||||
|
code: 'FAILED_TO_RESOLVE_REMOTE_CHANNEL',
|
||||||
|
id: '701c2582-fd94-11ec-a43c-3c7c3f10cc28',
|
||||||
|
kind: 'server',
|
||||||
|
},
|
||||||
|
|
||||||
noSuchChannel: {
|
noSuchChannel: {
|
||||||
message: 'No such channel.',
|
message: 'No such channel.',
|
||||||
code: 'NO_SUCH_CHANNEL',
|
code: 'NO_SUCH_CHANNEL',
|
||||||
|
@ -24,10 +34,25 @@ export const meta = {
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
anyOf: [
|
||||||
channelId: { type: 'string', format: 'misskey:id' },
|
{
|
||||||
},
|
properties: {
|
||||||
required: ['channelId'],
|
channelId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['channelId'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
properties: {
|
||||||
|
channelName: { type: 'string' },
|
||||||
|
host: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
description: 'The local host is represented with `null`.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['channelName'],
|
||||||
|
},
|
||||||
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
@ -36,6 +61,19 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
id: ps.channelId,
|
id: ps.channelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (typeof ps.host === 'string' && typeof ps.channelName === 'string') {
|
||||||
|
user = await resolveChannel(ps.channelName, ps.host).catch(e => {
|
||||||
|
apiLogger.warn(`failed to resolve remote channel: ${e}`);
|
||||||
|
throw new ApiError(meta.errors.failedToResolveRemoteChannel);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const q: FindOptionsWhere<Channel> = ps.channelId != null
|
||||||
|
? { id: ps.channelId }
|
||||||
|
: { usernameLower: ps.username!.toLowerCase(), host: IsNull() };
|
||||||
|
|
||||||
|
user = await Channels.findOneBy(q);
|
||||||
|
}
|
||||||
|
|
||||||
if (channel == null) {
|
if (channel == null) {
|
||||||
throw new ApiError(meta.errors.noSuchChannel);
|
throw new ApiError(meta.errors.noSuchChannel);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js';
|
import { Users, Notes, UserProfiles, Pages, Channels, Clips, GalleryPosts } from '@/models/index.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
|
import * as Grp from '@/misc/grp.js';
|
||||||
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
import { getNoteSummary } from '@/misc/get-note-summary.js';
|
||||||
import { queues } from '@/queue/queues.js';
|
import { queues } from '@/queue/queues.js';
|
||||||
import { genOpenapiSpec } from '../api/openapi/gen-spec.js';
|
import { genOpenapiSpec } from '../api/openapi/gen-spec.js';
|
||||||
|
@ -389,9 +390,26 @@ router.get('/gallery/:post', async (ctx, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
router.get('/channels/:channel', async (ctx, next) => {
|
router.get('/channels/:channel', async ctx => {
|
||||||
const channel = await Channels.findOneBy({
|
const channel = await Channels.findOneBy({
|
||||||
id: ctx.params.channel,
|
id: ctx.params.channel,
|
||||||
|
host: IsNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channel == null) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.redirect(`/+${channel.slug}${channel.host == null ? '' : '@' + channel.host}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/\\+:channel', async (ctx, next) => {
|
||||||
|
const { name, host } = Grp.parse(ctx.params.channel);
|
||||||
|
|
||||||
|
const channel = await Channels.findOneBy({
|
||||||
|
slug: name,
|
||||||
|
host: host ?? IsNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel) {
|
if (channel) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ extends ./base
|
||||||
|
|
||||||
block vars
|
block vars
|
||||||
- const title = channel.name;
|
- const title = channel.name;
|
||||||
- const url = `${config.url}/channels/${channel.id}`;
|
- const url = `${config.url}/+${(channel.host ? `${channel.slug}@${channel.host}` : channel.slug)}`;
|
||||||
|
|
||||||
block title
|
block title
|
||||||
= `${title} | ${instanceName}`
|
= `${title} | ${instanceName}`
|
||||||
|
|
|
@ -2,10 +2,12 @@ import Router from '@koa/router';
|
||||||
|
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
|
import * as Grp from '@/misc/grp.js';
|
||||||
import { links } from './nodeinfo.js';
|
import { links } from './nodeinfo.js';
|
||||||
import { escapeAttribute, escapeValue } from '@/prelude/xml.js';
|
import { escapeAttribute, escapeValue } from '@/prelude/xml.js';
|
||||||
import { Users } from '@/models/index.js';
|
import { Users, Channels } from '@/models/index.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { Channel } from '@/models/entities/channel.js';
|
||||||
import { FindOptionsWhere, IsNull } from 'typeorm';
|
import { FindOptionsWhere, IsNull } from 'typeorm';
|
||||||
|
|
||||||
// Init router
|
// Init router
|
||||||
|
@ -66,75 +68,132 @@ router.get('/.well-known/change-password', async ctx => {
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
router.get(webFingerPath, async ctx => {
|
const lookupUserById = (id: string) => {
|
||||||
const fromId = (id: User['id']): FindOptionsWhere<User> => ({
|
const query = {
|
||||||
id,
|
id,
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
} as FindOptionsWhere<User>;
|
||||||
|
return lookupUser(query);
|
||||||
|
};
|
||||||
|
|
||||||
const generateQuery = (resource: string): FindOptionsWhere<User> | number =>
|
const lookupUserByName = (resource: string) => {
|
||||||
resource.startsWith(`${config.url.toLowerCase()}/users/`) ?
|
const acct = Acct.parse(resource);
|
||||||
fromId(resource.split('/').pop()!) :
|
if (acct.host && acct.host !== config.host.toLowerCase()) {
|
||||||
fromAcct(Acct.parse(
|
return 422;
|
||||||
resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop()! :
|
};
|
||||||
resource.startsWith('acct:') ? resource.slice('acct:'.length) :
|
const query = {
|
||||||
resource));
|
usernameLower: acct.username,
|
||||||
|
host: IsNull(),
|
||||||
|
isSuspended: false,
|
||||||
|
} as FindOptionsWhere<User>;
|
||||||
|
return lookupUser(query);
|
||||||
|
};
|
||||||
|
|
||||||
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<User> | number =>
|
const lookupUser = async (query: FindOptionsWhere<User>) => {
|
||||||
!acct.host || acct.host === config.host.toLowerCase() ? {
|
const user = await Users.findOneBy(query);
|
||||||
usernameLower: acct.username,
|
|
||||||
host: IsNull(),
|
|
||||||
isSuspended: false,
|
|
||||||
} : 422;
|
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = `acct:${user.username}@${config.host}`;
|
||||||
|
const self = {
|
||||||
|
rel: 'self',
|
||||||
|
type: 'application/activity+json',
|
||||||
|
href: `${config.url}/users/${user.id}`,
|
||||||
|
};
|
||||||
|
const profilePage = {
|
||||||
|
rel: 'http://webfinger.net/rel/profile-page',
|
||||||
|
type: 'text/html',
|
||||||
|
href: `${config.url}/@${user.username}`,
|
||||||
|
};
|
||||||
|
const subscribe = {
|
||||||
|
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
||||||
|
template: `${config.url}/authorize-follow?acct={uri}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { subject, links: [self, profilePage, subscribe] };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const lookupChannelById = (id: string) => {
|
||||||
|
const query = {
|
||||||
|
id,
|
||||||
|
host: IsNull(),
|
||||||
|
} as FindOptionsWhere<Channel>;
|
||||||
|
return lookupChannel(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupChannelByName = (resource: string) => {
|
||||||
|
const grp = Grp.parse(resource);
|
||||||
|
if (grp.host && grp.host !== config.host.toLowerCase()) {
|
||||||
|
return 422;
|
||||||
|
};
|
||||||
|
const query = {
|
||||||
|
slug: grp.name,
|
||||||
|
host: IsNull(),
|
||||||
|
} as FindOptionsWhere<Channel>;
|
||||||
|
return lookupChannel(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lookupChannel = async (query: FindOptionsWhere<Channel>) => {
|
||||||
|
const channel = await Channels.findOneBy(query);
|
||||||
|
|
||||||
|
if (channel == null) {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = `grp:${channel.slug}@${config.host}`;
|
||||||
|
const self = {
|
||||||
|
rel: 'self',
|
||||||
|
type: 'application/activity+json',
|
||||||
|
href: `${config.url}/channel/${channel.id}`,
|
||||||
|
};
|
||||||
|
const profilePage = {
|
||||||
|
rel: 'http://webfinger.net/rel/profile-page',
|
||||||
|
type: 'text/html',
|
||||||
|
href: `${config.url}/+${channel.slug}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { subject, links: [self, profilePage] };
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get(webFingerPath, async ctx => {
|
||||||
if (typeof ctx.query.resource !== 'string') {
|
if (typeof ctx.query.resource !== 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = generateQuery(ctx.query.resource.toLowerCase());
|
const resource = ctx.query.resource.toLowerCase();
|
||||||
|
|
||||||
if (typeof query === 'number') {
|
let elements: object | number;
|
||||||
ctx.status = query;
|
if (resource.startsWith(`${config.url.toLowerCase()}/users/`) || resource.startsWith(`${config.url.toLowerCase()}/@`)) {
|
||||||
return;
|
elements = await lookupUserById(resource.split('/').pop()!);
|
||||||
|
} else if (resource.startsWith('acct:')) {
|
||||||
|
elements = await lookupUserByName(resource.slice('acct:'.length));
|
||||||
|
} else if (resource.startsWith(`${config.url.toLowerCase()}/channel/`) || resource.startsWith(`${config.url.toLowerCase()}/+`)) {
|
||||||
|
elements = await lookupChannelById(resource.split('/').pop()!);
|
||||||
|
} else if (resource.startsWith('grp:')) {
|
||||||
|
elements = await lookupChannelByName(resource.slice('grp:'.length));
|
||||||
|
} else {
|
||||||
|
elements = 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await Users.findOneBy(query);
|
if (typeof elements === 'number') {
|
||||||
|
ctx.status = elements;
|
||||||
if (user == null) {
|
|
||||||
ctx.status = 404;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subject = `acct:${user.username}@${config.host}`;
|
|
||||||
const self = {
|
|
||||||
rel: 'self',
|
|
||||||
type: 'application/activity+json',
|
|
||||||
href: `${config.url}/users/${user.id}`,
|
|
||||||
};
|
|
||||||
const profilePage = {
|
|
||||||
rel: 'http://webfinger.net/rel/profile-page',
|
|
||||||
type: 'text/html',
|
|
||||||
href: `${config.url}/@${user.username}`,
|
|
||||||
};
|
|
||||||
const subscribe = {
|
|
||||||
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
|
||||||
template: `${config.url}/authorize-follow?acct={uri}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ctx.accepts(jrd, xrd) === xrd) {
|
if (ctx.accepts(jrd, xrd) === xrd) {
|
||||||
ctx.body = XRD(
|
ctx.body = XRD(
|
||||||
{ element: 'Subject', value: subject },
|
{ element: 'Subject', value: elements.subject },
|
||||||
{ element: 'Link', attributes: self },
|
...elements.links.map((link) => ({
|
||||||
{ element: 'Link', attributes: profilePage },
|
element: 'Link', attributes: link,
|
||||||
{ element: 'Link', attributes: subscribe });
|
})));
|
||||||
ctx.type = xrd;
|
ctx.type = xrd;
|
||||||
} else {
|
} else {
|
||||||
ctx.body = {
|
ctx.body = elements;
|
||||||
subject,
|
|
||||||
links: [self, profilePage, subscribe],
|
|
||||||
};
|
|
||||||
ctx.type = jrd;
|
ctx.type = jrd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="channelPage(appearNote.channel)"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||||
|
@ -122,6 +122,7 @@ import { pleaseLogin } from '@/scripts/please-login';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
|
import { channelPage } from '@/filters/channel';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore, noteViewInterruptors } from '@/store';
|
import { defaultStore, noteViewInterruptors } from '@/store';
|
||||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import { url } from '@/config';
|
||||||
|
|
||||||
|
const normalize = (channel: misskey.entities.Channel) => {
|
||||||
|
return channel.host == null ? channel.slug : `${channel.slug}@${channel.host}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const channelPage = (channel: misskey.entities.Channel, path?, absolute = false) => {
|
||||||
|
return `${absolute ? url : ''}/+${normalize(channel)}${(path ? `/${path}` : '')}`;
|
||||||
|
};
|
|
@ -46,7 +46,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
channelId: {
|
channelName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ export default defineComponent({
|
||||||
channelId: {
|
channelId: {
|
||||||
async handler() {
|
async handler() {
|
||||||
this.channel = await os.api('channels/show', {
|
this.channel = await os.api('channels/show', {
|
||||||
channelId: this.channelId,
|
channelName: this.channelName,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true
|
||||||
|
|
|
@ -44,7 +44,7 @@ const defaultRoutes = [
|
||||||
{ path: '/channels', component: page('channels') },
|
{ path: '/channels', component: page('channels') },
|
||||||
{ path: '/channels/new', component: page('channel-editor') },
|
{ path: '/channels/new', component: page('channel-editor') },
|
||||||
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
|
||||||
{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) },
|
{ path: '/+:channel', component: page('channel'), props: route => ({ channelName: route.params.channel }) },
|
||||||
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
|
{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) },
|
||||||
{ path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) },
|
{ path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) },
|
||||||
{ path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) },
|
{ path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) },
|
||||||
|
|
Loading…
Reference in New Issue