Compare commits
5 Commits
e3311d91e7
...
fa602801f2
Author | SHA1 | Date |
---|---|---|
Natty | fa602801f2 | |
Natty | ddf7e07481 | |
Natty | 86c4804f9b | |
Natty | fada62950d | |
Natty | d70e98394f |
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
apiVersion: v2
|
||||
name: misskey
|
||||
version: 0.0.0
|
|
@ -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
|
|
@ -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 }}
|
|
@ -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
|
|
@ -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 }}
|
|
@ -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 }}
|
|
@ -1,3 +0,0 @@
|
|||
url: https://example.tld/
|
||||
image: okteto.dev/misskey
|
||||
environment: production
|
103
cliff.toml
103
cliff.toml
|
@ -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
|
Binary file not shown.
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
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue