Compare commits

...
25 changed files with 229 additions and 382 deletions
+9 -9
View File
@@ -1,24 +1,24 @@
# Contributing to Fluxer
This policy applies to all issues, discussions, commits and pull requests.
This policy applies to all commits and pull requests.
## Scope
To prevent spam, only approved contributors may submit pull requests.
To request approval, comment on an existing issue and ask to implement it. For work that extends beyond a defect fix, open a [discussion](https://github.com/orgs/fluxerapp/discussions) first.
To request approval, comment on the [feedback.fluxer.com](https://feedback.fluxer.com) post you want to implement and ask to work on it. For work that extends beyond a defect fix, post a feature request there first.
Every pull request must:
- Target the repository's default branch.
- Include a closing reference for each repository issue it resolves.
- Link each feedback.fluxer.com post it resolves.
- Receive approval from a maintainer before it is merged.
Place each closing reference on a separate line:
Place each link on a separate line:
```text
Closes #123
Closes #456
Resolves https://feedback.fluxer.com/p/123
Resolves https://feedback.fluxer.com/p/456
```
## Authorship
@@ -78,11 +78,11 @@ Complete every section of the pull request template. Clearly describe:
## Reports and other contributions
Use the [bug report form](https://github.com/fluxerapp/fluxer/issues/new?template=bug-report.yaml) to report reproducible defects.
Report bugs and request features at [feedback.fluxer.com](https://feedback.fluxer.com).
Report security vulnerabilities privately through the channels specified in the [security policy](https://github.com/fluxerapp/fluxer/blob/main/.github/SECURITY.md). Do not report vulnerabilities in public issues or discussions.
Report security vulnerabilities privately through [fluxer.app/security](https://fluxer.app/security). Never post them publicly.
Use [discussions](https://github.com/orgs/fluxerapp/discussions) for feature proposals and self-hosting questions.
Read the [operator documentation](https://fluxer.dev) for self-hosting questions.
Submit translations through [Weblate](https://weblate.fluxer.tools), not through pull requests.
-41
View File
@@ -1,41 +0,0 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-discussion.json
body:
- type: markdown
attributes:
value: |
Search existing discussions before posting a feature proposal.
Report vulnerabilities through the [private form](https://github.com/fluxerapp/fluxer/security/advisories/new) or <[email protected]>.
- type: textarea
id: problem
attributes:
label: Current problem
description: State what you are trying to do and what prevents it.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed change
description: State the expected behaviour.
validations:
required: true
- type: textarea
id: notes
attributes:
label: Additional information
description: Optional. Include constraints, trade-offs, related discussions, screenshots or mockups.
validations:
required: false
- type: checkboxes
id: checks
attributes:
label: Acknowledgements
options:
- label: I searched existing discussions.
required: true
-83
View File
@@ -1,83 +0,0 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json
name: Bug report
description: Report a reproducible defect in Fluxer.
type: Bug
body:
- type: markdown
attributes:
value: |
Search [open and closed issues](https://github.com/fluxerapp/fluxer/issues?q=is%3Aissue) before filing a report.
Report vulnerabilities through the [private form](https://github.com/fluxerapp/fluxer/security/advisories/new) or <[email protected]>. Send account and billing requests to <[email protected]>.
- type: textarea
id: summary
attributes:
label: Observed behaviour
description: State what happened and what you expected.
validations:
required: true
- type: textarea
id: steps
attributes:
label: Reproduction steps
description: Give numbered steps starting from a fresh app or session.
placeholder: |
1. Go to ...
2. Select ...
3. Observe ...
validations:
required: true
- type: input
id: build
attributes:
label: Build information
description: >-
Open User Settings, scroll to the bottom of the left sidebar, and select
the build information. Fluxer copies it to the clipboard.
validations:
required: true
- type: dropdown
id: surface
attributes:
label: Affected surface
multiple: true
options:
- Desktop app
- Web app
- Voice, video, or Go Live
- Self-hosted instance
- HTTP API or Gateway
- Documentation site
validations:
required: true
- type: input
id: instance
attributes:
label: Instance
description: For a self-hosted instance, include the release tag and database backend.
placeholder: fluxer.app
validations:
required: false
- type: textarea
id: evidence
attributes:
label: Evidence
description: Attach relevant logs, screenshots or recordings. Remove tokens, keys, private messages and other personal data. Configuration files may contain secrets.
validations:
required: false
- type: checkboxes
id: checks
attributes:
label: Acknowledgements
options:
- label: I searched open and closed issues.
required: true
- label: I removed secrets and unrelated personal data from the report.
required: true
-18
View File
@@ -1,18 +0,0 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Mobile client bugs
url: https://github.com/fluxerapp/flutter_client#bug-reporting
about: Read the reporting instructions for the Fluxer mobile client.
- name: Account and billing support
url: https://fluxer.app/help
about: Find account help and support contact details.
- name: Feature proposals
url: https://github.com/orgs/fluxerapp/discussions
about: Propose a feature in a discussion.
- name: Translations
url: https://weblate.fluxer.tools
about: Improve an existing locale or start a new one.
- name: Self-hosting support
url: https://fluxer.dev
about: Read the operator documentation, then open a discussion if the problem remains.
-44
View File
@@ -1,44 +0,0 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-forms.json
name: Documentation
description: Report incorrect, missing or unclear documentation.
type: Task
labels:
- docs
body:
- type: markdown
attributes:
value: |
This form covers <https://fluxer.dev> and operator documentation.
- type: textarea
id: issue
attributes:
label: Documentation defect
description: State what the page says and what is correct. For missing content, state what information you needed.
validations:
required: true
- type: input
id: location
attributes:
label: Location
description: Provide the page URL or file path and heading.
placeholder: https://fluxer.dev/gateway/overview/
validations:
required: false
- type: textarea
id: suggestion
attributes:
label: Proposed wording
description: Optional.
validations:
required: false
- type: checkboxes
id: checks
attributes:
label: Acknowledgements
options:
- label: I searched open and closed issues.
required: true
+2 -2
View File
@@ -1,7 +1,7 @@
# Security policy
Do not report a vulnerability in an issue, pull request, or discussion.
Do not report a vulnerability in a pull request or on feedback.fluxer.com.
Submit a report through [GitHub private vulnerability reporting](https://github.com/fluxerapp/fluxer/security/advisories/new) or email <security@fluxer.com>. Include the affected component, impact, reproduction steps, and supporting evidence. Remove unrelated personal data and secrets.
Submit a report through <https://fluxer.app/security> or email <security@fluxer.com>. Include the affected component, impact, reproduction steps, and supporting evidence. Remove unrelated personal data and secrets.
The programme scope, testing rules, safe harbour, disclosure process, and reward terms are published at <https://fluxer.app/security>. That page is authoritative.
+2 -2
View File
@@ -1,6 +1,6 @@
Closes #
Resolves https://feedback.fluxer.com/p/
<!-- Repeat this line for each resolved issue, up to 20. Remove the placeholder only if no issue is resolved and the approval gate does not apply. -->
<!-- Repeat this line for each feedback.fluxer.com post this resolves, up to 20. Remove the placeholder only if no post is resolved and the approval gate does not apply. -->
## Summary
+3
View File
@@ -23,6 +23,9 @@
# Fluxer
> [!IMPORTANT]
> Bug reports and feature requests have moved to [feedback.fluxer.com](https://feedback.fluxer.com). Sign in with your Fluxer account to post, vote and follow updates. GitHub Issues and Discussions are closed. Report security vulnerabilities privately through [fluxer.app/security](https://fluxer.app/security).
Fluxer is a free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
<p align="center">
@@ -69,6 +69,7 @@ import {shouldUseKeyboardShortcutsOverlayFallbackFromEvent} from '@app/features/
import {jsKeyToUiohookKeycode} from '@app/features/input/utils/UiohookKeycodes';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import MessageFocus from '@app/features/messaging/state/MessageFocus';
import {getSortedDmChannels} from '@app/features/messaging/utils/DmChannelUtils';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import Navigation from '@app/features/navigation/state/Navigation';
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
@@ -374,7 +375,7 @@ class KeybindManager {
}
private cycleDirectMessageContext(direction: 1 | -1): void {
const dmChannels = Channels.dmChannels;
const dmChannels = getSortedDmChannels(Channels.dmChannels, Authentication.currentUserId);
const slotCount = dmChannels.length + 1;
const currentChannelId = this.currentChannelId;
const currentIndex = currentChannelId ? dmChannels.findIndex((channel) => channel.id === currentChannelId) + 1 : 0;
@@ -926,6 +926,7 @@ export async function crosspost(i18n: I18n, channelId: string, messageId: string
export function deleteLocal(channelId: string, messageId: string): void {
logger.debug(`Deleting message ${messageId} locally in channel ${channelId}`);
Messages.handleMessageDelete({id: messageId, channelId});
MessageReply.handleMessageDelete(channelId, messageId);
}
export function revealMessage(channelId: string, messageId: string | null): void {
@@ -3,6 +3,7 @@
import ChannelPins from '@app/features/channel/state/ChannelPins';
import type {GatewayHandlerContext} from '@app/features/gateway/events/EventRouter';
import MessageReferences from '@app/features/messaging/state/MessageReferences';
import MessageReply from '@app/features/messaging/state/MessageReply';
import Messages from '@app/features/messaging/state/MessagingMessages';
import SavedMessages from '@app/features/messaging/state/SavedMessages';
import MentionFeed from '@app/features/notification/state/MentionFeed';
@@ -20,6 +21,7 @@ export function handleMessageDelete(data: MessageDeletePayload, _context: Gatewa
ChannelPins.handleMessageDelete(data.channel_id, data.id);
Messages.handleMessageDelete({channelId: data.channel_id, id: data.id});
MessageReferences.handleMessageDelete(data.channel_id, data.id);
MessageReply.handleMessageDelete(data.channel_id, data.id);
ReadStates.handleMessageDelete({channelId: data.channel_id});
MentionFeed.handleMessageDelete(data.id);
Notification.handleMessageDelete({channelId: data.channel_id});
@@ -2,6 +2,7 @@
import type {GatewayHandlerContext} from '@app/features/gateway/events/EventRouter';
import MessageReferences from '@app/features/messaging/state/MessageReferences';
import MessageReply from '@app/features/messaging/state/MessageReply';
import Messages from '@app/features/messaging/state/MessagingMessages';
import ReadStates from '@app/features/read_state/state/ReadStates';
import Notification from '@app/features/ui/state/Notification';
@@ -14,6 +15,7 @@ interface MessageDeleteBulkPayload {
export function handleMessageDeleteBulk(data: MessageDeleteBulkPayload, _context: GatewayHandlerContext): void {
Messages.handleMessageDeleteBulk({channelId: data.channel_id, ids: data.ids});
MessageReferences.handleMessageDeleteBulk(data.channel_id, data.ids);
MessageReply.handleMessageDeleteBulk(data.channel_id, data.ids);
ReadStates.handleMessageDelete({channelId: data.channel_id});
Notification.handleMessageDelete({channelId: data.channel_id});
}
@@ -1,170 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import MessageEdit from '@app/features/messaging/state/MessageEdit';
import MessageFocus from '@app/features/messaging/state/MessageFocus';
import {insertTextAtCursor} from '@app/features/messaging/utils/TextInputEditUtils';
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
import {canFocusTextarea, safeFocus} from '@app/features/platform/utils/InputFocusManager';
import {isTextInputKeyEvent} from '@app/features/platform/utils/IsTextInputKeyEvent';
import QuickSwitcher from '@app/features/search/state/QuickSwitcher';
import ContextMenu from '@app/features/ui/state/ContextMenu';
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import {useCallback, useEffect} from 'react';
interface UseTextareaKeyboardOptions {
channelId: string;
isFocused: boolean;
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
value: string;
setValue: React.Dispatch<React.SetStateAction<string>>;
handleTextChange: (newValue: string, previousValue: string) => void;
previousValueRef: React.RefObject<string>;
clearSegments: () => void;
replyingMessage: {
messageId: string;
mentioning: boolean;
} | null;
editingMessage: Message | null;
getLastEditableMessage: () => Message | null;
enabled: boolean;
}
type ArrowUpEditShortcutEvent = Pick<
React.KeyboardEvent | KeyboardEvent,
'key' | 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'defaultPrevented'
>;
export const shouldStartLastMessageEditFromArrowUp = (event: ArrowUpEditShortcutEvent, value: string): boolean => {
if (event.defaultPrevented) return false;
if (event.key !== 'ArrowUp') return false;
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return false;
return value.length === 0;
};
export const useTextareaKeyboard = ({
channelId,
isFocused,
textareaRef,
value,
setValue,
handleTextChange,
previousValueRef,
clearSegments,
replyingMessage,
editingMessage,
getLastEditableMessage,
enabled,
}: UseTextareaKeyboardOptions) => {
const mobileLayout = MobileLayout;
const editingMessageId = MessageEdit.getEditingMessageId(channelId);
useEffect(() => {
if (!enabled) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
const textarea = textareaRef.current;
if (!canFocusTextarea(textarea || undefined)) {
return;
}
if (isFocused) {
return;
}
if (QuickSwitcher.getIsOpen()) {
return;
}
if (ContextMenu.contextMenu) {
return;
}
if (KeyboardMode.keyboardModeEnabled && MessageFocus.focusedMessageId) {
return;
}
if (!isTextInputKeyEvent(event)) {
return;
}
if (!textarea) {
return;
}
if (event.key === 'Dead') {
safeFocus(textarea, true);
return;
}
event.preventDefault();
safeFocus(textarea, true);
const inserted = insertTextAtCursor(textarea, event.key);
if (!inserted) {
setValue((prev) => {
const newValue = prev + event.key;
handleTextChange(newValue, previousValueRef.current ?? '');
return newValue;
});
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [
editingMessageId,
isFocused,
mobileLayout.enabled,
handleTextChange,
previousValueRef,
textareaRef,
setValue,
enabled,
]);
useEffect(() => {
if (!enabled) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
const isEditingInline = MessageEdit.getEditingMessageId(channelId) != null;
if (isEditingInline) {
event.preventDefault();
event.stopPropagation();
MessageCommands.stopEdit(channelId);
return;
}
if (editingMessage && mobileLayout.enabled) {
event.preventDefault();
MessageCommands.stopEditMobile(channelId);
setValue('');
clearSegments();
} else if (replyingMessage) {
event.preventDefault();
MessageCommands.stopReply(channelId);
} else {
event.preventDefault();
ComponentBus.dispatch('ESCAPE_PRESSED');
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [channelId, replyingMessage, editingMessage, mobileLayout.enabled, clearSegments, setValue, enabled]);
const handleArrowUp = useCallback(
(event: React.KeyboardEvent) => {
if (!shouldStartLastMessageEditFromArrowUp(event, value)) {
return;
}
if (KeyboardMode.keyboardModeEnabled) {
event.preventDefault();
ComponentBus.dispatch('FOCUS_BOTTOMMOST_MESSAGE', {channelId});
return;
}
const message = getLastEditableMessage();
if (!message) {
return;
}
event.preventDefault();
MessageCommands.startEdit(channelId, message.id, message.content);
},
[channelId, value, getLastEditableMessage],
);
return {handleArrowUp};
};
@@ -13,7 +13,6 @@ import {emojiEquals} from '@app/features/messaging/utils/ReactionUtils';
import Relationships from '@app/features/relationship/state/Relationships';
import * as ThemeUtils from '@app/features/theme/utils/ThemeUtils';
import {User} from '@app/features/user/models/User';
import UserGuildSettings from '@app/features/user/state/UserGuildSettings';
import Users from '@app/features/user/state/Users';
import {LRUMap} from '@app/lib/list/ListLruMap';
import {MessageFlags, MessageStates, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
@@ -560,15 +559,23 @@ export class Message {
}
}
export const messageMentionsCurrentUser = (message: WireMessage): boolean => {
interface MentionSuppression {
suppressEveryone?: boolean;
suppressRoles?: boolean;
}
export const messageMentionsCurrentUser = (
message: WireMessage,
{suppressEveryone = false, suppressRoles = false}: MentionSuppression = {},
): boolean => {
const channel = Channels.getChannel(message.channel_id);
if (!channel) return false;
if (message.mention_everyone && !UserGuildSettings.isEveryoneMentionSuppressed(channel.guildId ?? null)) return true;
if (message.mention_everyone && !suppressEveryone) return true;
if (message.mentions?.some((user) => user.id === Authentication.currentUserId)) {
return true;
}
if (!channel.guildId) return false;
if (UserGuildSettings.isRoleMentionSuppressed(channel.guildId)) return false;
if (suppressRoles) return false;
const guild = Guilds.getGuild(channel.guildId);
if (!guild) return false;
const guildMember = GuildMembers.getMember(guild.id, Authentication.currentUserId);
@@ -56,6 +56,19 @@ class MessageReply {
delete this.replyingMessageIds[channelId];
}
handleMessageDelete(channelId: string, messageId: string): void {
if (this.replyingMessageIds[channelId]?.messageId === messageId) {
delete this.replyingMessageIds[channelId];
}
}
handleMessageDeleteBulk(channelId: string, messageIds: Array<string>): void {
const current = this.replyingMessageIds[channelId];
if (current && messageIds.includes(current.messageId)) {
delete this.replyingMessageIds[channelId];
}
}
highlightMessage(messageId: string): void {
this.highlightMessageId = messageId;
}
@@ -232,7 +232,12 @@ class Messages {
getLastEditableMessage(channelId: string): Message | undefined {
return this.getMessages(channelId).searchFromNewest((message) => {
return message.isCurrentUserAuthor() && message.state === MessageStates.SENT && message.isUserMessage();
return (
message.isCurrentUserAuthor() &&
message.state === MessageStates.SENT &&
message.isUserMessage() &&
!message.messageSnapshots
);
});
}
@@ -11,6 +11,7 @@ import {
} from '@app/features/notification/utils/MentionFeedFilters';
import Relationships from '@app/features/relationship/state/Relationships';
import {makeSyncedField} from '@app/features/user/state/SyncedField';
import UserGuildSettings from '@app/features/user/state/UserGuildSettings';
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
import {MAX_MESSAGES_PER_CHANNEL} from '@fluxer/constants/src/LimitConstants';
import type {Channel as WireChannel} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
@@ -179,14 +180,19 @@ class MentionFeed {
}
handleMessageCreate(message: WireMessage): void {
if (!messageMentionsCurrentUser(message)) {
const channel = Channels.getChannel(message.channel_id);
if (!channel) return;
const guildId = channel.guildId ?? null;
const mentioned = messageMentionsCurrentUser(message, {
suppressEveryone: UserGuildSettings.isEveryoneMentionSuppressed(guildId),
suppressRoles: UserGuildSettings.isRoleMentionSuppressed(guildId),
});
if (!mentioned) {
return;
}
if (Relationships.isBlocked(message.author.id)) {
return;
}
const channel = Channels.getChannel(message.channel_id);
if (!channel) return;
if (!this.isMessageIncludedByFilters(message, channel)) {
return;
}
@@ -47,7 +47,7 @@
<image type="source" width="2560" height="2227">https://fluxer.app/static/img/screenshots-desktop-2560w.948e9d88f4c9807a.avif</image>
</screenshot>
</screenshots>
<url type="bugtracker">https://github.com/fluxerapp/fluxer/issues</url>
<url type="bugtracker">https://feedback.fluxer.com</url>
<url type="homepage">https://fluxer.app</url>
<url type="donation">https://fluxer.app/donate</url>
<url type="contact">https://fluxer.app/company-information#contact</url>
@@ -47,7 +47,7 @@
<image type="source" width="1920" height="1670">https://fluxer.app/static/img/screenshots-desktop-1920w.4976d2d1f2238c05.avif</image>
</screenshot>
</screenshots>
<url type="bugtracker">https://github.com/fluxerapp/fluxer/issues</url>
<url type="bugtracker">https://feedback.fluxer.com</url>
<url type="homepage">https://canary.fluxer.app</url>
<url type="donation">https://fluxer.app/donate</url>
<url type="vcs-browser">https://github.com/fluxerapp/fluxer.git</url>
+1 -1
View File
@@ -215,7 +215,7 @@ function buildTemplate(): Array<MenuItemConstructorOptions> {
{
label: t('desktop.appMenu.reportIssue'),
click: async () => {
await openExternalDeduped('https://github.com/fluxerapp/fluxer/issues');
await openExternalDeduped('https://feedback.fluxer.com');
},
},
{type: 'separator'},
@@ -2198,6 +2198,7 @@ async function verifyInstallerExecution(installerRoot: string): Promise<Array<st
env: {
...process.env,
PATH: `${stubBin}${path.delimiter}${process.env.PATH ?? ''}`,
FLUXER_INSTALLER_REFRESHED: '1',
...(cwd == null ? {} : {PWD: cwd}),
},
});
@@ -240,7 +240,7 @@ Once email is on, the address goes through a DNS check before the account exists
Sign in to the admin dashboard at `https://chat.example.com/admin` with the account holding the wildcard ACL. The **Instance Config** page has everything the wizard asked, plus registration mode, approvals and integration keys. **Limit Config** holds the instance limits published to clients. **Voice Regions** and **Voice Servers** come seeded, so voice needs no setup there.
The desktop client opens the hosted web app for its release channel, so reach your own instance in a browser. [Issue #1088](https://github.com/fluxerapp/fluxer/issues/1088) tracks that, and is being worked on.
The desktop client opens the hosted web app for its release channel, so reach your own instance in a browser. [This feedback post](https://feedback.fluxer.com/p/641) tracks that, and is being worked on.
[Configuration](/operator/configuration/) lists every runtime setting and the environment variable each one overrides. [Deployment availability](/http-api/deployment-availability/) lists the routes that exist only on the hosted deployment.
@@ -128,6 +128,8 @@ Removing the line from `.env` also lets the upgrade run, and it is the wrong fix
Keep the installer beside the stack files. [Get started](/operator/get-started/#step-4-bring-up-the-instance) has the download and the checksum check for a fresh copy.
`--update` first compares the installer against the digest at `https://fluxer.dev/install.sh.sha256`. When the copy differs, it downloads the current one, checks it against that digest, and asks before it replaces the copy and runs it with the same options. Answering no, `--non-interactive`, or a run without a terminal stops before anything changes. On Windows the same check reads `https://fluxer.dev/install.ps1.sha256`.
See the plan first:
```bash
+75
View File
@@ -59,11 +59,17 @@ param(
[string[]]$Rest = @()
)
$FluxerScriptArguments = @{}
foreach ($entry in $PSBoundParameters.GetEnumerator()) {
$FluxerScriptArguments[$entry.Key] = $entry.Value
}
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$FluxerRawBase = 'https://raw.githubusercontent.com/fluxerapp/fluxer'
$FluxerInstallerUrl = 'https://fluxer.dev/install.ps1'
$FluxerStackPath = 'deploy/self-hosting'
$FluxerHealthPath = '/_health'
$FluxerInitService = 'seaweedfs-init'
@@ -1980,6 +1986,71 @@ function Assert-FluxerComposeFiles([string]$TargetDir, [string]$EnvPath) {
}
}
function Get-FluxerSha256([string]$Path) {
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
function Update-FluxerInstaller {
if ($env:FLUXER_INSTALLER_REFRESHED) {
return
}
$self = $PSCommandPath
if (-not $self) {
return
}
$selfDir = Split-Path -Parent $self
if ($DryRun) {
$staging = New-FluxerStagingDirectory ([System.IO.Path]::GetTempPath())
} else {
$staging = New-FluxerStagingDirectory $selfDir
}
try {
$digestPath = Join-Path $staging 'install.ps1.sha256'
try {
Invoke-WebRequest -Uri "$FluxerInstallerUrl.sha256" -OutFile $digestPath -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 60
} catch {
Write-FluxerLine "Could not reach $FluxerInstallerUrl.sha256, so this run goes on with $self."
return
}
$published = ([System.IO.File]::ReadAllText($digestPath).Trim() -split '\s+')[0].ToLowerInvariant()
if ($published -notmatch '^[0-9a-f]{64}$') {
Stop-Fluxer "$FluxerInstallerUrl.sha256 holds no sha256 digest. Nothing was changed." $FluxerExitDownload
}
if ((Get-FluxerSha256 $self) -eq $published) {
return
}
$fresh = Join-Path $staging 'install.ps1'
try {
Invoke-WebRequest -Uri $FluxerInstallerUrl -OutFile $fresh -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 120
} catch {
Stop-Fluxer "Download failed for $FluxerInstallerUrl. Nothing was changed." $FluxerExitDownload
}
if ((Get-FluxerSha256 $fresh) -ne $published) {
Stop-Fluxer "$FluxerInstallerUrl does not match the digest in $FluxerInstallerUrl.sha256. Nothing was changed." $FluxerExitDownload
}
Write-FluxerLine "$self differs from the installer $FluxerInstallerUrl serves. The stack files an upgrade downloads can require .env keys that only the current installer writes."
if ($DryRun) {
Write-FluxerLine 'The run asks to replace it with the current installer before it changes anything. The plan below is the one this copy would follow.'
return
}
if ($NonInteractive -or [Console]::IsInputRedirected) {
Stop-Fluxer "Nothing was changed. Download the current installer and run it:`n Invoke-WebRequest -Uri $FluxerInstallerUrl -OutFile install.ps1 -UseBasicParsing" $FluxerExitRefused
}
$answer = Read-Host -Prompt "Replace $self with the current installer and run that? [y/N]"
if ($null -eq $answer -or $answer.Trim() -notmatch '^(y|yes)$') {
Stop-Fluxer "Kept $self. Nothing was changed. Read the current installer at $FluxerInstallerUrl and run it once it is in place." $FluxerExitRefused
}
Move-Item -LiteralPath $fresh -Destination $self -Force
} finally {
Remove-FluxerStagingDirectory $staging
}
Write-FluxerLine "Replaced $self. Running it."
$env:FLUXER_INSTALLER_REFRESHED = '1'
$global:LASTEXITCODE = 0
& $self @FluxerScriptArguments
exit $LASTEXITCODE
}
function Invoke-FluxerInstall {
if ($Help) {
Show-FluxerUsage
@@ -2022,6 +2093,10 @@ function Invoke-FluxerInstall {
Invoke-FluxerPreflight
if ($Update) {
Update-FluxerInstaller
}
$targetPath = $Dir
$adoptedCwd = $false
$fellBack = $false
+85
View File
@@ -51,6 +51,7 @@ LC_ALL=C
export LC_ALL
FLUXER_RAW_BASE='https://raw.githubusercontent.com/fluxerapp/fluxer'
FLUXER_INSTALLER_URL='https://fluxer.dev/install.sh'
FLUXER_STACK_PATH='deploy/self-hosting'
FLUXER_MIN_ENGINE='24.0.0'
# Podman numbers its releases on its own scale, so the Docker Engine floor says
@@ -305,6 +306,18 @@ opt_no_volume_compression=0
opt_skip_backup=0
opt_allow_root=0
fluxer_self=''
if [ -f "$0" ]; then
case $0 in
/*) fluxer_self=$0 ;;
*) fluxer_self="$(pwd)/$0" ;;
esac
fi
fluxer_self_args=''
for fluxer_arg in "$@"; do
fluxer_self_args="$fluxer_self_args '$(printf '%s' "$fluxer_arg" | sed "s/'/'\\\\''/g")'"
done
while [ $# -gt 0 ]; do
case $1 in
--domain)
@@ -651,6 +664,74 @@ fluxer_prompt() {
return 1
}
fluxer_sha256() {
openssl dgst -sha256 < "$1" | awk '{print $NF}'
}
fluxer_drop_scratch() {
fluxer_cleanup
fluxer_scratch=''
}
fluxer_refresh_installer() {
[ -z "${FLUXER_INSTALLER_REFRESHED:-}" ] || return 0
[ -n "$fluxer_self" ] || return 0
fluxer_self_dir=$(dirname "$fluxer_self")
if [ "$opt_dry_run" -eq 1 ]; then
fluxer_open_scratch "${TMPDIR:-/tmp}"
elif [ -w "$fluxer_self_dir" ]; then
fluxer_open_scratch "$fluxer_self_dir"
else
fluxer_say "$fluxer_self_dir is not writable, so this run cannot check $fluxer_self against $FLUXER_INSTALLER_URL and goes on with it."
return 0
fi
if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$fluxer_scratch/install.sh.sha256" "$FLUXER_INSTALLER_URL.sha256"; then
fluxer_say "Could not reach $FLUXER_INSTALLER_URL.sha256, so this run goes on with $fluxer_self."
fluxer_drop_scratch
return 0
fi
fluxer_published=$(awk 'NR == 1 {print $1}' "$fluxer_scratch/install.sh.sha256")
case $fluxer_published in
''|*[!0-9a-f]*) fluxer_fail 4 "$FLUXER_INSTALLER_URL.sha256 holds no sha256 digest. Nothing was changed." ;;
esac
if [ "$(fluxer_sha256 "$fluxer_self")" = "$fluxer_published" ]; then
fluxer_drop_scratch
return 0
fi
if ! curl -fsSL --proto '=https' --tlsv1.2 -o "$fluxer_scratch/install.sh" "$FLUXER_INSTALLER_URL"; then
fluxer_fail 4 "Download failed for $FLUXER_INSTALLER_URL. Nothing was changed."
fi
if [ "$(fluxer_sha256 "$fluxer_scratch/install.sh")" != "$fluxer_published" ]; then
fluxer_fail 4 "$FLUXER_INSTALLER_URL does not match the digest in $FLUXER_INSTALLER_URL.sha256. Nothing was changed."
fi
fluxer_say "$fluxer_self differs from the installer $FLUXER_INSTALLER_URL serves. The stack files an upgrade downloads can require .env keys that only the current installer writes."
if [ "$opt_dry_run" -eq 1 ]; then
fluxer_say 'The run asks to replace it with the current installer before it changes anything. The plan below is the one this copy would follow.'
fluxer_drop_scratch
return 0
fi
if [ "$opt_non_interactive" -eq 1 ] || [ ! -t 0 ]; then
fluxer_fail 3 "Nothing was changed. Download the current installer and run it:
curl -fsSLO $FLUXER_INSTALLER_URL"
fi
printf 'Replace %s with the current installer and run that? [y/N] ' "$fluxer_self" >&2
fluxer_answer=''
read -r fluxer_answer || true
case $fluxer_answer in
y|Y|yes|Yes|YES) ;;
*) fluxer_fail 3 "Kept $fluxer_self. Nothing was changed. Read the current installer at $FLUXER_INSTALLER_URL and run it once it is in place." ;;
esac
if [ -x "$fluxer_self" ]; then
chmod +x "$fluxer_scratch/install.sh"
fi
mv "$fluxer_scratch/install.sh" "$fluxer_self"
fluxer_drop_scratch
fluxer_say "Replaced $fluxer_self. Running it."
FLUXER_INSTALLER_REFRESHED=1
export FLUXER_INSTALLER_REFRESHED
eval "exec sh \"\$fluxer_self\" $fluxer_self_args"
}
fluxer_resolve_values() {
if [ "$opt_update" -eq 1 ] || [ "$opt_rollback" -eq 1 ]; then
return 0
@@ -2100,6 +2181,10 @@ fluxer_preflight
fluxer_validate_options
fluxer_resolve_values
if [ "$opt_update" -eq 1 ]; then
fluxer_refresh_installer
fi
if [ "$opt_update" -eq 1 ] || [ "$opt_rollback" -eq 1 ]; then
fluxer_require_instance
fluxer_resolve_ref