Compare commits

..

5 Commits

Author SHA1 Message Date
Natty fa602801f2
Replaced messaging with search in the mobile view
ci/woodpecker/tag/ociImageTag Pipeline was successful Details
2023-04-22 13:01:48 +02:00
Natty ddf7e07481
Removed NodeInfo and WebFinger endpoints 2023-04-22 12:47:12 +02:00
Natty 86c4804f9b
Removed the NSFW model 2023-04-21 19:23:22 +02:00
Natty fada62950d
No Procfile 2023-04-21 19:02:45 +02:00
Natty d70e98394f
No helm charts 2023-04-21 19:02:17 +02:00
23 changed files with 173 additions and 1105 deletions

View File

@ -1,10 +1,7 @@
root = true
[*]
indent_style = tab
indent_size = 2
indent_style = space
indent_size = 4
charset = utf-8
insert_final_newline = true
[*.yml]
indent_style = space

View File

@ -1 +0,0 @@
web: NODE_ENV=production npm start

View File

@ -1,72 +0,0 @@
# Replace example.tld with your domain
# For WebSocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
server {
listen 80;
listen [::]:80;
server_name example.tld;
# For SSL domain validation
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
location /.well-known/pki-validation/ { allow all; }
location / { return 301 https://$server_name$request_uri; }
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.tld;
ssl_session_timeout 1d;
ssl_session_cache shared:ssl_session_cache:10m;
ssl_session_tickets off;
# To use Let's Encrypt certificate
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
# SSL protocol settings
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_stapling on;
ssl_stapling_verify on;
# Change to your upload limit
client_max_body_size 80m;
# Proxy to Node
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_redirect off;
# If it's behind another reverse proxy or CDN, remove the following.
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# For WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Cache settings
proxy_cache cache1;
proxy_cache_lock on;
proxy_cache_use_stale updating;
add_header X-Cache $upstream_cache_status;
}
}

View File

@ -1,3 +0,0 @@
apiVersion: v2
name: misskey
version: 0.0.0

View File

@ -1,162 +0,0 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
# url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# URL SETTINGS AFTER THAT!
# ┌───────────────────────┐
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
#port: 443
#https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: localhost
port: 5432
# Database name
db: misskey
# Auth
user: example-misskey-user
pass: example-misskey-pass
# Whether disable Caching queries
#disableCache: true
# Extra Connection options
#extra:
# ssl: true
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: localhost
port: 6379
#pass: example-pass
#prefix: example-prefix
#db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────
#elasticsearch:
# host: localhost
# port: 9200
# ssl: false
# user:
# pass:
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
# You can select the ID generation method.
# You don't usually need to change this setting, but you can
# change it according to your preferences.
# Available methods:
# aid ... Short, Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: "aid"
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 16
# Job attempts
# deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
#proxyBypassHosts: [
# 'example.com',
# '192.0.2.8'
#]
# Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@ -1,8 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "misskey.fullname" . }}-configuration
data:
default.yml: |-
{{ .Files.Get "files/default.yml"|nindent 4 }}
url: {{ .Values.url }}

View File

@ -1,47 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "misskey.fullname" . }}
labels:
{{- include "misskey.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "misskey.selectorLabels" . | nindent 6 }}
replicas: 1
template:
metadata:
labels:
{{- include "misskey.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: misskey
image: {{ .Values.image }}
env:
- name: NODE_ENV
value: {{ .Values.environment }}
volumeMounts:
- name: {{ include "misskey.fullname" . }}-configuration
mountPath: /misskey/.config
readOnly: true
ports:
- containerPort: 3000
- name: postgres
image: postgres:14-alpine
env:
- name: POSTGRES_USER
value: "example-misskey-user"
- name: POSTGRES_PASSWORD
value: "example-misskey-pass"
- name: POSTGRES_DB
value: "misskey"
ports:
- containerPort: 5432
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
volumes:
- name: {{ include "misskey.fullname" . }}-configuration
configMap:
name: {{ include "misskey.fullname" . }}-configuration

View File

@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "misskey.fullname" . }}
annotations:
dev.okteto.com/auto-ingress: "true"
spec:
type: ClusterIP
ports:
- port: 3000
protocol: TCP
name: http
selector:
{{- include "misskey.selectorLabels" . | nindent 4 }}

View File

@ -1,62 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "misskey.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "misskey.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "misskey.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "misskey.labels" -}}
helm.sh/chart: {{ include "misskey.chart" . }}
{{ include "misskey.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "misskey.selectorLabels" -}}
app.kubernetes.io/name: {{ include "misskey.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "misskey.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -1,3 +0,0 @@
url: https://example.tld/
image: okteto.dev/misskey
environment: production

View File

@ -1,103 +0,0 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All changes from v13.0.0 onwards, for a full list of differences read CALCKEY.md\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = false
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^add", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^prevent", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^🎨", group = "Refactor"},
{ message = "^enhance", group = "Refactor"},
{ message = "^⚡️", group = "Refactor"},
{ message = "^🔥", group = "Features"},
{ message = "^🐛", group = "Bug Fixes"},
{ message = "^🚑️", group = "Bug Fixes"},
{ message = "^block", group = "Bug Fixes"},
{ message = "^✨", group = "Features"},
{ message = "^📝", group = "Documentation"},
{ message = "^🚀", group = "Features"},
{ message = "^💄", group = "Styling"},
{ message = "^✅", group = "Testing"},
{ message = "^🔒️", group = "Security"},
{ message = "^🚨", group = "Testing"},
{ message = "^💚", group = "CI"},
{ message = "^👷", group = "CI"},
{ message = "^⬇️", group = "Miscellaneous Tasks"},
{ message = "^⬆️", group = "Miscellaneous Tasks"},
{ message = "^📌", group = "Miscellaneous Tasks"},
{ message = "^", group = "Miscellaneous Tasks"},
{ message = "^", group = "Miscellaneous Tasks"},
{ message = "^♻️", group = "Refactor"},
{ message = "^🔧", group = "CI"},
{ message = "^🔨", group = "CI"},
{ message = "^🌐", group = "Localization"},
{ message = "^✏️", group = "Localization"},
{ message = "^👽️", group = "Bug Fixes"},
{ message = "^🍱", group = "Styling"},
{ message = "^♿️", group = "Styling"},
{ message = "^🩹", group = "Bug Fixes"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ message = "^update", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,6 @@ import { publishMainStream } from "@/services/stream.js";
import * as Acct from "@/misc/acct.js";
import { envOption } from "@/env.js";
import activityPub from "./activitypub.js";
import nodeinfo from "./nodeinfo.js";
import wellKnown from "./well-known.js";
import apiServer from "./api/index.js";
import fileServer from "./file/index.js";
@ -36,30 +35,30 @@ const app = new Koa();
app.proxy = true;
if (!["production", "test"].includes(process.env.NODE_ENV || "")) {
// Logger
app.use(
koaLogger((str) => {
serverLogger.info(str);
}),
);
// Logger
app.use(
koaLogger((str) => {
serverLogger.info(str);
})
);
// Delay
if (envOption.slow) {
app.use(
slow({
delay: 3000,
}),
);
}
// Delay
if (envOption.slow) {
app.use(
slow({
delay: 3000,
})
);
}
}
// HSTS
// 6months (15552000sec)
if (config.url.startsWith("https") && !config.disableHsts) {
app.use(async (ctx, next) => {
ctx.set("strict-transport-security", "max-age=15552000; preload");
await next();
});
app.use(async (ctx, next) => {
ctx.set("strict-transport-security", "max-age=15552000; preload");
await next();
});
}
app.use(mount("/api", apiServer));
@ -71,66 +70,65 @@ const router = new Router();
// Routing
router.use(activityPub.routes());
router.use(nodeinfo.routes());
router.use(wellKnown.routes());
router.get("/avatar/@:acct", async (ctx) => {
const { username, host } = Acct.parse(ctx.params.acct);
const user = await Users.findOne({
where: {
usernameLower: username.toLowerCase(),
host: host == null || host === config.host ? IsNull() : host,
isSuspended: false,
},
relations: ["avatar"],
});
const { username, host } = Acct.parse(ctx.params.acct);
const user = await Users.findOne({
where: {
usernameLower: username.toLowerCase(),
host: host == null || host === config.host ? IsNull() : host,
isSuspended: false,
},
relations: ["avatar"],
});
if (user) {
ctx.redirect(Users.getAvatarUrlSync(user));
} else {
ctx.redirect("/static-assets/user-unknown.png");
}
if (user) {
ctx.redirect(Users.getAvatarUrlSync(user));
} else {
ctx.redirect("/static-assets/user-unknown.png");
}
});
router.get("/identicon/:x", async (ctx) => {
const [temp, cleanup] = await createTemp();
await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
ctx.set("Content-Type", "image/png");
ctx.body = fs.createReadStream(temp).on("close", () => cleanup());
const [temp, cleanup] = await createTemp();
await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
ctx.set("Content-Type", "image/png");
ctx.body = fs.createReadStream(temp).on("close", () => cleanup());
});
router.get("/verify-email/:code", async (ctx) => {
const profile = await UserProfiles.findOneBy({
emailVerifyCode: ctx.params.code,
});
const profile = await UserProfiles.findOneBy({
emailVerifyCode: ctx.params.code,
});
if (profile != null) {
ctx.body = "Verify succeeded!";
ctx.status = 200;
if (profile != null) {
ctx.body = "Verify succeeded!";
ctx.status = 200;
await UserProfiles.update(
{ userId: profile.userId },
{
emailVerified: true,
emailVerifyCode: null,
},
);
await UserProfiles.update(
{ userId: profile.userId },
{
emailVerified: true,
emailVerifyCode: null,
}
);
publishMainStream(
profile.userId,
"meUpdated",
await Users.pack(
profile.userId,
{ id: profile.userId },
{
detail: true,
includeSecrets: true,
},
),
);
} else {
ctx.status = 404;
}
publishMainStream(
profile.userId,
"meUpdated",
await Users.pack(
profile.userId,
{ id: profile.userId },
{
detail: true,
includeSecrets: true,
}
)
);
} else {
ctx.status = 404;
}
});
// Register router
@ -139,51 +137,51 @@ app.use(router.routes());
app.use(mount(webServer));
function createServer() {
return http.createServer(app.callback());
return http.createServer(app.callback());
}
// For testing
export const startServer = () => {
const server = createServer();
const server = createServer();
initializeStreamingServer(server);
initializeStreamingServer(server);
server.listen(config.port);
server.listen(config.port);
return server;
return server;
};
export default () =>
new Promise((resolve) => {
const server = createServer();
new Promise((resolve) => {
const server = createServer();
initializeStreamingServer(server);
initializeStreamingServer(server);
server.on("error", (e) => {
switch ((e as any).code) {
case "EACCES":
serverLogger.error(
`You do not have permission to listen on port ${config.port}.`,
);
break;
case "EADDRINUSE":
serverLogger.error(
`Port ${config.port} is already in use by another process.`,
);
break;
default:
serverLogger.error(e);
break;
}
server.on("error", (e) => {
switch ((e as any).code) {
case "EACCES":
serverLogger.error(
`You do not have permission to listen on port ${config.port}.`
);
break;
case "EADDRINUSE":
serverLogger.error(
`Port ${config.port} is already in use by another process.`
);
break;
default:
serverLogger.error(e);
break;
}
if (cluster.isWorker) {
process.send!("listenFailed");
} else {
// disableClustering
process.exit(1);
}
});
if (cluster.isWorker) {
process.send!("listenFailed");
} else {
// disableClustering
process.exit(1);
}
});
// @ts-ignore
server.listen(config.port, resolve);
});
// @ts-ignore
server.listen(config.port, resolve);
});

View File

@ -1,119 +0,0 @@
import Router from "@koa/router";
import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, Notes } from "@/models/index.js";
import { IsNull, MoreThan } from "typeorm";
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
import { Cache } from "@/misc/cache.js";
const router = new Router();
const nodeinfo2_1path = "/nodeinfo/2.1";
const nodeinfo2_0path = "/nodeinfo/2.0";
// to cleo: leave this http or bonks
export const links = [
{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
href: config.url + nodeinfo2_1path,
},
{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: config.url + nodeinfo2_0path,
},
];
const nodeinfo2 = async () => {
const now = Date.now();
const [meta, total, activeHalfyear, activeMonth, localPosts] =
await Promise.all([
fetchMeta(true),
Users.count({ where: { host: IsNull() } }),
Users.count({
where: {
host: IsNull(),
lastActiveDate: MoreThan(new Date(now - 15552000000)),
},
}),
Users.count({
where: {
host: IsNull(),
lastActiveDate: MoreThan(new Date(now - 2592000000)),
},
}),
Notes.count({ where: { userHost: IsNull() } }),
]);
const proxyAccount = meta.proxyAccountId
? await Users.pack(meta.proxyAccountId).catch(() => null)
: null;
return {
software: {
name: "calckey",
version: config.version,
repository: meta.repositoryUrl,
homepage: "https://calckey.cloud",
},
protocols: ["activitypub"],
services: {
inbound: [] as string[],
outbound: ["atom1.0", "rss2.0"],
},
openRegistrations: !meta.disableRegistration,
usage: {
users: { total, activeHalfyear, activeMonth },
localPosts,
localComments: 0,
},
metadata: {
nodeName: meta.name,
nodeDescription: meta.description,
maintainer: {
name: meta.maintainerName,
email: meta.maintainerEmail,
},
langs: meta.langs,
tosUrl: meta.ToSUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
disableLocalTimeline: meta.disableLocalTimeline,
disableRecommendedTimeline: meta.disableRecommendedTimeline,
disableGlobalTimeline: meta.disableGlobalTimeline,
emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
maxCaptionTextLength: MAX_CAPTION_TEXT_LENGTH,
enableTwitterIntegration: meta.enableTwitterIntegration,
enableGithubIntegration: meta.enableGithubIntegration,
enableDiscordIntegration: meta.enableDiscordIntegration,
enableEmail: meta.enableEmail,
enableServiceWorker: meta.enableServiceWorker,
proxyAccountName: proxyAccount ? proxyAccount.username : null,
themeColor: meta.themeColor || "#31748f",
},
};
};
const cache = new Cache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10);
router.get(nodeinfo2_1path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2());
ctx.body = { version: "2.1", ...base };
ctx.set("Cache-Control", "public, max-age=600");
});
router.get(nodeinfo2_0path, async (ctx) => {
const base = await cache.fetch(null, () => nodeinfo2());
// @ts-ignore
base.software.repository = undefined;
ctx.body = { version: "2.0", ...base };
ctx.set("Cache-Control", "public, max-age=600");
});
export default router;

View File

@ -1,34 +1,33 @@
import Router from "@koa/router";
import config from "@/config/index.js";
import * as Acct from "@/misc/acct.js";
import { links } from "./nodeinfo.js";
import { escapeAttribute, escapeValue } from "@/prelude/xml.js";
import { Users } from "@/models/index.js";
import type { User } from "@/models/entities/user.js";
import type { FindOptionsWhere } from "typeorm";
import { IsNull } from "typeorm";
import {escapeAttribute, escapeValue} from "@/prelude/xml.js";
// Init router
const router = new Router();
const XRD = (
...x: {
element: string;
value?: string;
attributes?: Record<string, string>;
}[]
...x: {
element: string;
value?: string;
attributes?: Record<string, string>;
}[]
) =>
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x
.map(
({ element, value, attributes }) =>
`<${Object.entries(
(typeof attributes === "object" && attributes) || {},
).reduce((a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`, element)}${
typeof value === "string" ? `>${escapeValue(value)}</${element}` : "/"
}>`,
)
.reduce((a, c) => a + c, "")}</XRD>`;
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">${x
.map(
({ element, value, attributes }) =>
`<${Object.entries(
(typeof attributes === "object" && attributes) || {}
).reduce(
(a, [k, v]) => `${a} ${k}="${escapeAttribute(v)}"`,
element
)}${
typeof value === "string"
? `>${escapeValue(value)}</${element}`
: "/"
}>`
)
.reduce((a, c) => a + c, "")}</XRD>`;
const allPath = "/.well-known/(.*)";
const webFingerPath = "/.well-known/webfinger";
@ -36,156 +35,68 @@ const jrd = "application/jrd+json";
const xrd = "application/xrd+xml";
router.use(allPath, async (ctx, next) => {
ctx.set({
"Access-Control-Allow-Headers": "Accept",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "Vary",
});
await next();
ctx.set({
"Access-Control-Allow-Headers": "Accept",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers": "Vary",
});
await next();
});
router.options(allPath, async (ctx) => {
ctx.status = 204;
ctx.status = 204;
});
router.get("/.well-known/host-meta", async (ctx) => {
ctx.set("Content-Type", xrd);
ctx.body = XRD({
element: "Link",
attributes: {
rel: "lrdd",
type: xrd,
template: `${config.url}${webFingerPath}?resource={uri}`,
},
});
ctx.set("Content-Type", xrd);
ctx.body = XRD({
element: "Link",
attributes: {
rel: "lrdd",
type: xrd,
template: `${config.url}${webFingerPath}?resource={uri}`,
},
});
});
router.get("/.well-known/host-meta.json", async (ctx) => {
ctx.set("Content-Type", jrd);
ctx.body = {
links: [
{
rel: "lrdd",
type: jrd,
template: `${config.url}${webFingerPath}?resource={uri}`,
},
],
};
ctx.set("Content-Type", jrd);
ctx.body = {
links: [
{
rel: "lrdd",
type: jrd,
template: `${config.url}${webFingerPath}?resource={uri}`,
},
],
};
});
if (config.twa != null) {
router.get("/.well-known/assetlinks.json", async (ctx) => {
ctx.set("Content-Type", "application/json");
ctx.body = [
{
relation: ["delegate_permission/common.handle_all_urls"],
target: {
namespace: config.twa.nameSpace,
package_name: config.twa.packageName,
sha256_cert_fingerprints: config.twa.sha256CertFingerprints,
},
},
];
});
router.get("/.well-known/assetlinks.json", async (ctx) => {
ctx.set("Content-Type", "application/json");
ctx.body = [
{
relation: ["delegate_permission/common.handle_all_urls"],
target: {
namespace: config.twa.nameSpace,
package_name: config.twa.packageName,
sha256_cert_fingerprints: config.twa.sha256CertFingerprints,
},
},
];
});
}
router.get("/.well-known/nodeinfo", async (ctx) => {
ctx.body = { links };
});
/* TODO
router.get('/.well-known/change-password', async ctx => {
});
*/
router.get(webFingerPath, async (ctx) => {
const fromId = (id: User["id"]): FindOptionsWhere<User> => ({
id,
host: IsNull(),
isSuspended: false,
});
const generateQuery = (resource: string): FindOptionsWhere<User> | number =>
resource.startsWith(`${config.url.toLowerCase()}/users/`)
? fromId(resource.split("/").pop()!)
: fromAcct(
Acct.parse(
resource.startsWith(`${config.url.toLowerCase()}/@`)
? resource.split("/").pop()!
: resource.startsWith("acct:")
? resource.slice("acct:".length)
: resource,
),
);
const fromAcct = (acct: Acct.Acct): FindOptionsWhere<User> | number =>
!acct.host || acct.host === config.host.toLowerCase()
? {
usernameLower: acct.username,
host: IsNull(),
isSuspended: false,
}
: 422;
if (typeof ctx.query.resource !== "string") {
ctx.status = 400;
return;
}
const query = generateQuery(ctx.query.resource.toLowerCase());
if (typeof query === "number") {
ctx.status = query;
return;
}
const user = await Users.findOneBy(query);
if (user == null) {
ctx.status = 404;
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) {
ctx.body = XRD(
{ element: "Subject", value: subject },
{ element: "Link", attributes: self },
{ element: "Link", attributes: profilePage },
{ element: "Link", attributes: subscribe },
);
ctx.type = xrd;
} else {
ctx.body = {
subject,
links: [self, profilePage, subscribe],
};
ctx.type = jrd;
}
ctx.vary("Accept");
ctx.set("Cache-Control", "public, max-age=180");
});
// Return 404 for other .well-known
router.all(allPath, async (ctx) => {
ctx.status = 404;
ctx.status = 404;
});
export default router;

View File

@ -1,200 +0,0 @@
<template>
<MkA
class="rivslvers"
tabindex="-1"
:class="{
isMe: isMe(message),
isRead: message.groupId
? message.reads.includes($i?.id)
: message.isRead,
}"
:to="
message.groupId
? `/my/messaging/group/${message.groupId}`
: `/my/messaging/${getAcct(
isMe(message) ? message.recipient : message.user
)}`
"
>
<div class="message _block">
<MkAvatar
class="avatar"
:user="
message.groupId
? message.user
: isMe(message)
? message.recipient
: message.user
"
:show-indicator="true"
/>
<header v-if="message.groupId">
<span class="name">{{ message.group.name }}</span>
<MkTime :time="message.createdAt" class="time" />
</header>
<header v-else>
<span class="name"
><MkUserName
:user="
isMe(message) ? message.recipient : message.user
"
/></span>
<span class="username"
>@{{
acct(isMe(message) ? message.recipient : message.user)
}}</span
>
<MkTime :time="message.createdAt" class="time" />
</header>
<div class="body">
<p class="text">
<span v-if="isMe(message)" class="me"
>{{ i18n.ts.you }}:
</span>
<Mfm
v-if="message.text != null && message.text.length > 0"
:text="message.text"
/>
<span v-else> 📎</span>
</p>
</div>
</div>
</MkA>
</template>
<script lang="ts" setup>
import * as Acct from "calckey-js/built/acct";
import { i18n } from "@/i18n";
import { acct } from "@/filters/user";
import { $i } from "@/account";
const getAcct = Acct.toString;
const props = defineProps<{
message: Record<string, any>;
}>();
function isMe(message): boolean {
return message.userId === $i?.id;
}
</script>
<style lang="scss" scoped>
.rivslvers {
> .message {
display: block;
text-decoration: none;
margin-bottom: var(--margin);
* {
pointer-events: none;
user-select: none;
}
&:hover {
.avatar {
filter: saturate(200%);
}
}
&.isRead,
&.isMe {
opacity: 0.8;
}
&:not(.isMe):not(.isRead) {
> div {
background-image: url("/client-assets/unread.svg");
background-repeat: no-repeat;
background-position: 0 center;
}
}
&:after {
content: "";
display: block;
clear: both;
}
padding: 20px 30px;
> header {
display: flex;
align-items: center;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
> .name {
margin: 0;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 1em;
font-weight: bold;
transition: all 0.1s ease;
}
> .username {
margin: 0 8px;
}
> .time {
margin: 0 0 0 auto;
}
}
> .avatar {
float: left;
width: 54px;
height: 54px;
margin: 0 16px 0 0;
border-radius: 8px;
transition: all 0.1s ease;
}
> .body {
> .text {
display: block;
margin: 0 0 0 0;
padding: 0;
overflow: hidden;
overflow-wrap: break-word;
text-decoration: none;
font-size: 1.1em;
color: var(--faceText);
.me {
opacity: 0.7;
}
}
> .image {
display: block;
max-width: 100%;
max-height: 512px;
}
}
}
&.max-width_400px {
> .message {
&:not(.isMe):not(.isRead) {
> div {
background-image: none;
border-left: solid 4px #3aa2dc;
}
}
> div {
padding: 16px;
font-size: 0.9em;
> .avatar {
margin: 0 12px 0 0;
}
}
}
}
}
</style>

View File

@ -59,6 +59,14 @@
<i class="ph-house ph-bold ph-lg"></i>
</div>
</button>
<button
class="button widget _button"
@click="search"
>
<div class="button-wrapper">
<i class="ph-magnifying-glass ph-bold ph-lg"></i>
</div>
</button>
<button
class="button notifications _button"
@click="
@ -68,7 +76,7 @@
>
<div
class="button-wrapper"
:class="buttonAnimIndex === 1 ? 'on' : ''"
:class="buttonAnimIndex === 2 ? 'on' : ''"
>
<i class="ph-bell ph-bold ph-lg"></i
><span v-if="$i?.hasUnreadNotification" class="indicator"
@ -76,25 +84,6 @@
></span>
</div>
</button>
<button
class="button messaging _button"
@click="
mainRouter.push('/my/messaging');
updateButtonState();
"
>
<div
class="button-wrapper"
:class="buttonAnimIndex === 2 ? 'on' : ''"
>
<i class="ph-chats-teardrop ph-bold ph-lg"></i
><span
v-if="$i?.hasUnreadMessagingMessage"
class="indicator"
><i class="ph-circle ph-fill"></i
></span>
</div>
</button>
<button
class="button widget _button"
@click="widgetsShowing = true"
@ -174,6 +163,7 @@ import {
setPageMetadata,
} from "@/scripts/page-metadata";
import { deviceKind } from "@/scripts/device-kind";
import {search} from "@/scripts/search";
const XWidgets = defineAsyncComponent(() => import("./universal.widgets.vue"));
const XSidebar = defineAsyncComponent(() => import("@/ui/_common_/navbar.vue"));
@ -225,10 +215,6 @@ function updateButtonState(): void {
return;
}
if (routerState.includes("/my/notifications")) {
buttonAnimIndex.value = 1;
return;
}
if (routerState.includes("/my/messaging")) {
buttonAnimIndex.value = 2;
return;
}