more mastodon work
This commit is contained in:
parent
74903846b6
commit
05acb51da2
11 changed files with 203 additions and 56 deletions
|
@ -98,6 +98,7 @@
|
|||
"punycode": "2.1.1",
|
||||
"pureimage": "0.3.15",
|
||||
"qrcode": "1.5.1",
|
||||
"qs": "6.9.7",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.18.0",
|
||||
|
@ -158,6 +159,7 @@
|
|||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.5.0",
|
||||
"@types/qs": "6.9.7",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
"@types/redis": "4.0.11",
|
||||
|
|
|
@ -71,26 +71,8 @@ export function apiAccountMastodon(router: Router): void {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
let userArray = ctx.query.acct?.toString().split("@");
|
||||
let userid;
|
||||
if (userArray === undefined) {
|
||||
ctx.status = 401;
|
||||
ctx.body = { error: "no user specified" };
|
||||
return;
|
||||
}
|
||||
if (userArray.length === 1) {
|
||||
const q: FindOptionsWhere<User> = {
|
||||
usernameLower: userArray[0].toLowerCase(),
|
||||
host: IsNull(),
|
||||
};
|
||||
|
||||
const user = await Users.findOneBy(q);
|
||||
userid = user?.id;
|
||||
} else {
|
||||
userid = (await resolveUser(userArray[0], userArray[1])).id;
|
||||
}
|
||||
const data = await client.getAccount(userid ? userid : "");
|
||||
ctx.body = data.data;
|
||||
const data = await client.search((request.query as any).acct, 'accounts');
|
||||
ctx.body = data.data.accounts[0];
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
|
|
|
@ -44,12 +44,10 @@ const writeScope = [
|
|||
export function apiAuthMastodon(router: Router): void {
|
||||
router.post("/v1/apps", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
const client = getClient(BASE_URL, '');
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
try {
|
||||
let scope = body.scopes;
|
||||
console.log(body);
|
||||
if (typeof scope === "string") scope = scope.split(" ");
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
|
@ -64,14 +62,16 @@ export function apiAuthMastodon(router: Router): void {
|
|||
redirect_uris: red,
|
||||
website: body.website,
|
||||
});
|
||||
ctx.body = {
|
||||
id: appData.id,
|
||||
const returns = {
|
||||
id: Math.floor(Math.random() * 100).toString(),
|
||||
name: appData.name,
|
||||
website: appData.website,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || "").toString("base64"),
|
||||
client_secret: appData.clientSecret,
|
||||
client_secret: appData.clientSecret
|
||||
};
|
||||
console.log(returns)
|
||||
ctx.body = returns;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
|
|
|
@ -10,18 +10,18 @@ export async function getInstance(response: Entity.Instance) {
|
|||
const totalStatuses = Notes.count({ where: { userHost: IsNull() } });
|
||||
return {
|
||||
uri: response.uri,
|
||||
title: response.title || "",
|
||||
short_description: response.description || "",
|
||||
description: response.description || "",
|
||||
title: response.title || "Calckey",
|
||||
short_description: response.description.substring(0, 50) || "See real server website",
|
||||
description: response.description || "This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)",
|
||||
email: response.email || "",
|
||||
version: "3.0.0 compatible (Calckey)",
|
||||
version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it.
|
||||
urls: response.urls,
|
||||
stats: {
|
||||
user_count: (await totalUsers),
|
||||
status_count: (await totalStatuses),
|
||||
domain_count: response.stats.domain_count
|
||||
},
|
||||
thumbnail: response.thumbnail || "",
|
||||
thumbnail: response.thumbnail || 'https://http.cat/404',
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: !response.registrations,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import megalodon, { MegalodonInterface } from "@calckey/megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import axios from "axios";
|
||||
import { Converter } from "@calckey/megalodon";
|
||||
import { limitToInt } from "./timeline.js";
|
||||
|
||||
export function apiSearchMastodon(router: Router): void {
|
||||
router.get("/v1/search", async (ctx) => {
|
||||
|
@ -9,7 +12,7 @@ export function apiSearchMastodon(router: Router): void {
|
|||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = ctx.request.body;
|
||||
try {
|
||||
const query: any = ctx.query;
|
||||
const query: any = limitToInt(ctx.query);
|
||||
const type = query.type || "";
|
||||
const data = await client.search(query.q, type, query);
|
||||
ctx.body = data.data;
|
||||
|
@ -19,4 +22,110 @@ export function apiSearchMastodon(router: Router): void {
|
|||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v2/search", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = limitToInt(ctx.query);
|
||||
const type = query.type;
|
||||
if (type) {
|
||||
const data = await client.search(query.q, type, query);
|
||||
ctx.body = data.data;
|
||||
} else {
|
||||
const acct = await client.search(query.q, "accounts", query);
|
||||
const stat = await client.search(query.q, "statuses", query);
|
||||
const tags = await client.search(query.q, "hashtags", query);
|
||||
ctx.body = {
|
||||
accounts: acct.data.accounts,
|
||||
statuses: stat.data.statuses,
|
||||
hashtags: tags.data.hashtags,
|
||||
};
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = (401);
|
||||
ctx.body e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v1/trends/statuses", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
try {
|
||||
const data = await getHighlight(BASE_URL, ctx.request.hostname, accessTokens);
|
||||
ctx.body = data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = (401);
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
router.get("/v2/suggestions", async (ctx) => {
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
try {
|
||||
const query: any = ctx.query;
|
||||
const data = await getFeaturedUser(
|
||||
BASE_URL,
|
||||
ctx.request.hostname,
|
||||
accessTokens,
|
||||
query.limit || 20,
|
||||
);
|
||||
console.log(data);
|
||||
ctx.body = data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = (401);
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
async function getHighlight(
|
||||
BASE_URL: string,
|
||||
domain: string,
|
||||
accessTokens: string | undefined,
|
||||
) {
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/notes/featured`, {
|
||||
i: accessToken,
|
||||
});
|
||||
const data: MisskeyEntity.Note[] = api.data;
|
||||
return data.map((note) => Converter.note(note, domain));
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async function getFeaturedUser(
|
||||
BASE_URL: string,
|
||||
host: string,
|
||||
accessTokens: string | undefined,
|
||||
limit: number,
|
||||
) {
|
||||
const accessTokenArr = accessTokens?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
try {
|
||||
const api = await axios.post(`${BASE_URL}/api/users`, {
|
||||
i: accessToken,
|
||||
limit,
|
||||
origin: "local",
|
||||
sort: "+follower",
|
||||
state: "alive",
|
||||
});
|
||||
const data: MisskeyEntity.UserDetail[] = api.data;
|
||||
console.log(data);
|
||||
return data.map((u) => {
|
||||
return {
|
||||
source: "past_interactions",
|
||||
account: Converter.userDetail(u, host),
|
||||
};
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
console.log(e.response.data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,12 @@ import Router from "@koa/router";
|
|||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
||||
import axios from "axios";
|
||||
import querystring from 'node:querystring'
|
||||
import qs from 'qs'
|
||||
function normalizeQuery(data: any) {
|
||||
const str = querystring.stringify(data);
|
||||
return qs.parse(str);
|
||||
}
|
||||
|
||||
export function apiStatusMastodon(router: Router): void {
|
||||
router.post("/v1/statuses", async (ctx) => {
|
||||
|
@ -9,9 +15,12 @@ export function apiStatusMastodon(router: Router): void {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const body: any = ctx.request.body;
|
||||
let body: any = ctx.request.body;
|
||||
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])) {
|
||||
body = normalizeQuery(body)
|
||||
}
|
||||
const text = body.status;
|
||||
const removed = text.replace(/@\S+/g, "").replaceAll(" ", "");
|
||||
const removed = text.replace(/@\S+/g, "").replace(/\s|/g, '')
|
||||
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
|
||||
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
|
||||
if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
|
||||
|
@ -36,6 +45,8 @@ export function apiStatusMastodon(router: Router): void {
|
|||
}
|
||||
if (!body.media_ids) body.media_ids = undefined;
|
||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||
const { sensitive } = body
|
||||
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive
|
||||
const data = await client.postStatus(text, body);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
|
@ -70,7 +81,7 @@ export function apiStatusMastodon(router: Router): void {
|
|||
const data = await client.deleteStatus(ctx.params.id);
|
||||
ctx.body = data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data, request.params.id);
|
||||
ctx.status = 401;
|
||||
ctx.body = e.response.data;
|
||||
}
|
||||
|
@ -430,6 +441,6 @@ export function statusModel(
|
|||
pinned: false,
|
||||
emoji_reactions: [],
|
||||
bookmarked: false,
|
||||
quote: false,
|
||||
quote: null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ export function limitToInt(q: ParsedUrlQuery) {
|
|||
let object: any = q;
|
||||
if (q.limit)
|
||||
if (typeof q.limit === "string") object.limit = parseInt(q.limit, 10);
|
||||
return q;
|
||||
if (q.offset)
|
||||
if (typeof q.offset === "string") object.offset = parseInt(q.offset, 10);
|
||||
return object;
|
||||
}
|
||||
|
||||
export function argsToBools(q: ParsedUrlQuery) {
|
||||
|
@ -26,12 +28,29 @@ export function argsToBools(q: ParsedUrlQuery) {
|
|||
export function toTextWithReaction(status: Entity.Status[], host: string) {
|
||||
return status.map((t) => {
|
||||
if (!t) return statusModel(null, null, [], "no content");
|
||||
t.quote = null as any;
|
||||
if (!t.emoji_reactions) return t;
|
||||
if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0];
|
||||
const reactions = t.emoji_reactions.map(
|
||||
(r) => `${r.name.replace("@.", "")} (${r.count}${r.me ? "* " : ""})`,
|
||||
);
|
||||
//t.emojis = getEmoji(t.content, host)
|
||||
const reactions = t.emoji_reactions.map((r) => {
|
||||
const emojiNotation = r.url ? `:${r.name.replace('@.', '')}:` : r.name
|
||||
return `${emojiNotation} (${r.count}${r.me ? `* ` : ''})`
|
||||
});
|
||||
const reaction = t.emoji_reactions as Entity.Reaction[];
|
||||
const emoji = t.emojis || []
|
||||
for (const r of reaction) {
|
||||
if (!r.url) continue
|
||||
emoji.push({
|
||||
'shortcode': r.name,
|
||||
'url': r.url,
|
||||
'static_url': r.url,
|
||||
'visible_in_picker': true,
|
||||
},)
|
||||
}
|
||||
const isMe = reaction.findIndex((r) => r.me) > -1;
|
||||
const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0);
|
||||
t.favourited = isMe;
|
||||
t.favourites_count = total;
|
||||
t.emojis = emoji
|
||||
t.content = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join(
|
||||
", ",
|
||||
)}</p>`;
|
||||
|
@ -103,7 +122,7 @@ export function apiTimelineMastodon(router: Router): void {
|
|||
}
|
||||
},
|
||||
);
|
||||
router.get<{ Params: { hashtag: string } }>(
|
||||
router.get(
|
||||
"/v1/timelines/home",
|
||||
async (ctx, reply) => {
|
||||
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
|
||||
|
|
|
@ -414,12 +414,13 @@ export default class Connection {
|
|||
const client = getClient(this.host, this.accessToken);
|
||||
client.getStatus(payload.id).then((data) => {
|
||||
const newPost = toTextWithReaction([data.data], this.host);
|
||||
const targetPost = newPost[0]
|
||||
for (const stream of this.currentSubscribe) {
|
||||
this.wsConnection.send(
|
||||
JSON.stringify({
|
||||
stream,
|
||||
event: "status.update",
|
||||
payload: JSON.stringify(newPost[0]),
|
||||
payload: JSON.stringify(targetPost),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -154,24 +154,29 @@ router.get("/verify-email/:code", async (ctx) => {
|
|||
});
|
||||
|
||||
mastoRouter.get("/oauth/authorize", async (ctx) => {
|
||||
const client_id = ctx.request.query.client_id;
|
||||
const { client_id, state, redirect_uri } = ctx.request.query.client_id;
|
||||
console.log(ctx.request.req);
|
||||
ctx.redirect(Buffer.from(client_id?.toString() || "", "base64").toString());
|
||||
const param = state ? `state=${state}&mastodon=true` : "mastodon=true";
|
||||
ctx.redirect(`${Buffer.from(client_id || '', 'base64').toString()}?${param}`);
|
||||
});
|
||||
|
||||
mastoRouter.post("/oauth/token", async (ctx) => {
|
||||
const body: any = ctx.request.body;
|
||||
const body: any = ctx.request.body || ctx.request.query;
|
||||
console.log('token-request', body)
|
||||
let client_id: any = ctx.request.query.client_id;
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator("misskey", BASE_URL, null) as MegalodonInterface;
|
||||
let m = null;
|
||||
let token = null;
|
||||
if (body.code) {
|
||||
m = body.code.match(/^[a-zA-Z0-9-]+/);
|
||||
m = body.code.match(/^([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/);
|
||||
if (!m.length) {
|
||||
ctx.body = { error: "Invalid code" };
|
||||
return;
|
||||
}
|
||||
token = `${m[1]}-${m[2]}-${m[3]}-${m[4]}-${m[5]}`
|
||||
console.log(body.code, token)
|
||||
}
|
||||
if (client_id instanceof Array) {
|
||||
client_id = client_id.toString();
|
||||
|
@ -182,14 +187,16 @@ mastoRouter.post("/oauth/token", async (ctx) => {
|
|||
const atData = await client.fetchAccessToken(
|
||||
client_id,
|
||||
body.client_secret,
|
||||
m ? m[0] : "",
|
||||
token ? token : "",
|
||||
);
|
||||
ctx.body = {
|
||||
const ret = {
|
||||
access_token: atData.accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: "read write follow",
|
||||
scope: body.scope || 'read write follow push',
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
console.log('token-response', ret)
|
||||
ctx.body = ret;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
ctx.status = 401;
|
||||
|
|
|
@ -86,7 +86,14 @@ export default defineComponent({
|
|||
accepted() {
|
||||
this.state = 'accepted';
|
||||
const getUrlParams = () => window.location.search.substring(1).split('&').reduce((result, query) => { const [k, v] = query.split('='); result[k] = decodeURI(v); return result; }, {});
|
||||
if (this.session.app.callbackUrl) {
|
||||
const isMastodon = !!getUrlParams().mastodon
|
||||
if (this.session.app.callbackUrl && isMastodon) {
|
||||
const state = getUrlParams().state
|
||||
const stateParam = `&state=${state}`
|
||||
const tokenRaw = this.session.token
|
||||
const token = tokenRaw.replaceAll('-', '')
|
||||
location.href = `${this.session.app.callbackUrl}?code=${token}${stateParam}`;
|
||||
} else if (this.session.app.callbackUrl) {
|
||||
const url = new URL(this.session.app.callbackUrl);
|
||||
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
|
||||
if (this.session.app.callbackUrl === "urn:ietf:wg:oauth:2.0:oob") {
|
||||
|
|
|
@ -100,6 +100,7 @@ importers:
|
|||
'@types/pug': 2.0.6
|
||||
'@types/punycode': 2.1.0
|
||||
'@types/qrcode': 1.5.0
|
||||
'@types/qs': 6.9.7
|
||||
'@types/random-seed': 0.3.3
|
||||
'@types/ratelimiter': 3.4.4
|
||||
'@types/redis': 4.0.11
|
||||
|
@ -183,6 +184,7 @@ importers:
|
|||
punycode: 2.1.1
|
||||
pureimage: 0.3.15
|
||||
qrcode: 1.5.1
|
||||
qs: 6.9.7
|
||||
random-seed: 0.3.0
|
||||
ratelimiter: 3.4.1
|
||||
re2: 1.18.0
|
||||
|
@ -295,6 +297,7 @@ importers:
|
|||
punycode: 2.1.1
|
||||
pureimage: 0.3.15
|
||||
qrcode: 1.5.1
|
||||
qs: 6.9.7
|
||||
random-seed: 0.3.0
|
||||
ratelimiter: 3.4.1
|
||||
re2: 1.18.0
|
||||
|
@ -357,6 +360,7 @@ importers:
|
|||
'@types/pug': 2.0.6
|
||||
'@types/punycode': 2.1.0
|
||||
'@types/qrcode': 1.5.0
|
||||
'@types/qs': 6.9.7
|
||||
'@types/random-seed': 0.3.3
|
||||
'@types/ratelimiter': 3.4.4
|
||||
'@types/redis': 4.0.11
|
||||
|
@ -4264,7 +4268,7 @@ packages:
|
|||
resolution: {integrity: sha512-sX/LQ7LqUhgyaxzbe7IqwPeTr2yfpfUIQ/dgpKo6ZI4y4lpQA0YxAomWIY+7I7rHWcG02PG+OuPREzMW/5tszQ==}
|
||||
dependencies:
|
||||
inflation: 2.0.0
|
||||
qs: 6.11.0
|
||||
qs: 6.9.7
|
||||
raw-body: 2.5.1
|
||||
type-is: 1.6.18
|
||||
dev: false
|
||||
|
@ -4273,7 +4277,7 @@ packages:
|
|||
resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==}
|
||||
dependencies:
|
||||
inflation: 2.0.0
|
||||
qs: 6.11.0
|
||||
qs: 6.9.7
|
||||
raw-body: 2.5.1
|
||||
type-is: 1.6.18
|
||||
dev: false
|
||||
|
@ -10691,6 +10695,11 @@ packages:
|
|||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/qs/6.9.7:
|
||||
resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==}
|
||||
engines: {node: '>=0.6'}
|
||||
dev: false
|
||||
|
||||
/query-string/4.3.4:
|
||||
resolution: {integrity: sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
Loading…
Reference in a new issue