Compare commits

...

10 Commits

Author SHA1 Message Date
Iris System 4d65237ee1 chore: remove old web-proxy code
This is no longer used in our infra, and probably should never have
been in this repo anyway

Closes: PluralKit/PluralKit#511
2023-06-28 18:45:11 +12:00
Iris System 6b14c50f09 fix(bot): only allow proxying in known-supported channel types
This is so that new channel types added by Discord (that may or may not
support the features we need for proxying to work) don't throw piles of
error codes at users when they try to proxy.
2023-06-28 14:38:50 +12:00
Iris System a0fa03599b fix(bot): display new Discord usernames correctly 2023-06-28 13:55:21 +12:00
Jake Fulmine 11d70666d0 fix(dashboard): typo 2023-06-11 11:08:57 +02:00
Jake Fulmine 99f7044bab fix(dashboard): fix broken styling in list 2023-06-11 11:08:00 +02:00
Jake Fulmine ac91093847 fix(dashboard): pass all arguments to list filtering (fix broken member/group pages) 2023-06-04 09:46:44 +02:00
Jake Fulmine c3ade8f455 fix(dashboard): show filtered group/member count 2023-06-03 22:08:03 +02:00
Jake Fulmine b4e7f9a6c3 fix(dashboard): fix group searching 2023-06-03 20:57:34 +02:00
Jake Fulmine 96622c5bd4 fix(dashboard): correctly show group count in group list 2023-06-03 20:37:30 +02:00
Jake Fulmine 434ed3e50c fix(dashboard): add viewing/sorting by creation date on public page 2023-06-03 13:36:11 +02:00
20 changed files with 61 additions and 155 deletions

View File

@ -1,19 +0,0 @@
name: Deploy web proxy to Fly.io
on:
push:
branches: [main]
paths:
- 'services/web-proxy/**'
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: (cd services/web-proxy && flyctl deploy)

View File

@ -14,7 +14,10 @@ public record Channel
GuildNewsThread = 10,
GuildPublicThread = 11,
GuildPrivateThread = 12,
GuildStageVoice = 13
GuildStageVoice = 13,
GuildDirectory = 14,
GuildForum = 15,
GuildMedia = 16,
}
public ulong Id { get; init; }

View File

@ -23,6 +23,7 @@ public record User
public ulong Id { get; init; }
public string Username { get; init; }
public string Discriminator { get; init; }
public string GlobalName { get; init; }
public string? Avatar { get; init; }
public bool Bot { get; init; }
public bool? System { get; init; }

View File

@ -248,10 +248,10 @@ public class Checks
// Run everything through the checks, catch the ProxyCheckFailedException, and reply with the error message.
try
{
_proxy.ShouldProxy(channel, msg, context);
_proxy.ShouldProxy(channel, rootChannel, msg, context);
_matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, true, ctx.Config.CaseSensitiveProxyTags);
var canProxy = await _proxy.CanProxy(channel, msg, context);
var canProxy = await _proxy.CanProxy(channel, rootChannel, msg, context);
if (canProxy != null)
{
await ctx.Reply(canProxy);

View File

@ -57,7 +57,9 @@ public class ProxyService
public async Task<bool> HandleIncomingMessage(MessageCreateEvent message, MessageContext ctx,
Guild guild, Channel channel, bool allowAutoproxy, PermissionSet botPermissions)
{
if (!ShouldProxy(channel, message, ctx))
var rootChannel = await _cache.GetRootChannel(message.ChannelId);
if (!ShouldProxy(channel, rootChannel, message, ctx))
return false;
var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, guild.Id, null);
@ -72,8 +74,6 @@ public class ProxyService
return false;
}
var rootChannel = await _cache.GetRootChannel(message.ChannelId);
List<ProxyMember> members;
// Fetch members and try to match to a specific member
using (_metrics.Measure.Timer.Time(BotMetrics.ProxyMembersQueryTime))
@ -82,7 +82,7 @@ public class ProxyService
if (!_matcher.TryMatch(ctx, autoproxySettings, members, out var match, message.Content, message.Attachments.Length > 0,
allowAutoproxy, ctx.CaseSensitiveProxyTags)) return false;
var canProxy = await CanProxy(channel, message, ctx);
var canProxy = await CanProxy(channel, rootChannel, message, ctx);
if (canProxy != null)
{
if (ctx.ProxyErrorMessageEnabled)
@ -109,8 +109,32 @@ public class ProxyService
return true;
}
public async Task<string> CanProxy(Channel channel, Message msg, MessageContext ctx)
#pragma warning disable CA1822 // Mark members as static
internal bool CanProxyInChannel(Channel ch, bool isRootChannel = false)
#pragma warning restore CA1822 // Mark members as static
{
// this is explicitly selecting known channel types so that when Discord add new
// ones, users don't get flooded with error codes if that new channel type doesn't
// support a feature we need for proxying
return ch.Type switch
{
Channel.ChannelType.GuildText => true,
Channel.ChannelType.GuildPublicThread => true,
Channel.ChannelType.GuildPrivateThread => true,
Channel.ChannelType.GuildNews => true,
Channel.ChannelType.GuildNewsThread => true,
Channel.ChannelType.GuildVoice => true,
Channel.ChannelType.GuildStageVoice => true,
Channel.ChannelType.GuildForum => isRootChannel,
_ => false,
};
}
public async Task<string> CanProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
{
if (!(CanProxyInChannel(channel) && CanProxyInChannel(rootChannel, true)))
return $"PluralKit cannot proxy messages in this type of channel.";
// Check if the message does not go over any Discord Nitro limits
if (msg.Content != null && msg.Content.Length > 2000)
{
@ -132,7 +156,7 @@ public class ProxyService
return null;
}
public bool ShouldProxy(Channel channel, Message msg, MessageContext ctx)
public bool ShouldProxy(Channel channel, Channel rootChannel, Message msg, MessageContext ctx)
{
// Make sure author has a system
if (ctx.SystemId == null)
@ -386,7 +410,7 @@ public class ProxyService
content.Append($"*[(click to see attachment)]({jumpLink})*");
}
var username = nickname ?? repliedTo.Author.Username;
var username = nickname ?? repliedTo.Author.GlobalName ?? repliedTo.Author.Username;
var avatarUrl = avatar != null
? $"https://cdn.discordapp.com/guilds/{trigger.GuildId}/users/{repliedTo.Author.Id}/avatars/{avatar}.png"
: $"https://cdn.discordapp.com/avatars/{repliedTo.Author.Id}/{repliedTo.Author.Avatar}.png";

View File

@ -105,7 +105,14 @@ public class WebhookCacheService
{
try
{
return await _rest.GetChannelWebhooks(channelId);
var webhooks = await _rest.GetChannelWebhooks(channelId);
if (webhooks != null)
return webhooks;
// Getting a 404 / null response from the above generally means the channel type does
// not support webhooks - this is detected elsewhere for proxying purposes, let's just
// return an empty array here
return new Webhook[0];
}
catch (HttpRequestException e)
{

View File

@ -35,7 +35,7 @@ public static class DiscordUtils
private static readonly Regex UNBROKEN_LINK_REGEX = new("<?(https?:\\/\\/[^\\s<]+[^<.,:;\"')\\]\\s])>?");
public static string NameAndMention(this User user) =>
$"{user.Username}#{user.Discriminator} ({user.Mention()})";
$"{user.Username}{(user.Discriminator == "0" ? "" : $"#{user.Discriminator}")} ({user.Mention()})";
public static Instant SnowflakeToInstant(ulong snowflake) =>
Instant.FromUtc(2015, 1, 1, 0, 0, 0) + Duration.FromMilliseconds(snowflake >> 22);

View File

@ -85,7 +85,7 @@
<b>Display Name:</b> <span bind:this={displayNameElement}><AwaitHtml htmlPromise={htmlDisplayNamePromise} /></span>
</Col>
{/if}
{#if group.created && !isPublic}
{#if group.created}
<Col xs={12} lg={4} class="mb-2">
<b>Created:</b> {created}
</Col>

View File

@ -76,7 +76,7 @@
</Col>
</Row>
{:else}
<span class="itemcounter">{members.length} {pageOptions.type}s ({currentGroups.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
<span class="itemcounter">{processedGroups.length} {pageOptions.type}s ({currentGroups.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
@ -88,7 +88,7 @@
{:else if pageOptions.view === "tiny"}
tiny!
{:else}
<ListView currentList={currentGroups} {pageOptions} {options} fullListLength={members.length} />
<ListView currentList={currentGroups} {pageOptions} {options} fullListLength={groups.length} />
{/if}
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />
{/if}

View File

@ -143,9 +143,7 @@ function resetPage() {
<option value="birthday">Birthday</option>
{/if}
<option value="color">Color</option>
{#if !pageOptions.isPublic}
<option value="created">Creation date</option>
{/if}
<option value="none">API response order</option>
</Input>
</InputGroup>

View File

@ -147,8 +147,8 @@
{:else if settings.accessibility.expandedcards}
{#each currentList as item, index (pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
<Card class="mb-3">
<button class="accordion-button collapsed p-0" id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} tabindex={0}>
<CardHeader>
<h2 class="accordion-header card-header">
<button class="w-100 accordion-button collapsed bg-transparent" id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} tabindex={0}>
<CardsHeader {item} sortBy={options.sort}>
<button class="button-reset" slot="icon" style="cursor: pointer;" id={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown|stopPropagation={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
{#if item.privacy && item.privacy.visibility === "private"}
@ -161,8 +161,8 @@
</button>
</CardsHeader>
<Tooltip placement="top" target={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`}>{copiedArray[indexStart + index] ? "Copied!" : "Copy public link"}</Tooltip>
</CardHeader>
</button>
</button>
</h2>
<CardBody>
{#if pageOptions.type === "member"}
<MemberBody isPublic={pageOptions.isPublic} member={item} />
@ -176,7 +176,7 @@
<div class="my-3">
{#each currentList as item, index(pageOptions.randomized ? item.uuid + '-' + index : item.uuid)}
<Card style="border-radius: 0;">
<a class="accordion-button collapsed bg-transparent" style="text-decoration: none;" href={getItemLink(item)} id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} use:link >
<a class="accordion-button p-3 collapsed bg-transparent" style="text-decoration: none;" href={getItemLink(item)} id={`${pageOptions.type}-card-${indexStart + index}`} on:keydown={(e) => skipToNextItem(e, indexStart + index)} use:link >
<CardsHeader {item} sortBy={options.sort}>
<button class="button-reset" slot="icon" style="cursor: pointer;" id={`${pageOptions.type}-copy-${item.id}-${indexStart + index}`} on:click|stopPropagation={() => copyShortLink(indexStart + index, item.id)} on:keydown|stopPropagation={(e) => copyShortLink(indexStart + index, item.id, e)} tabindex={0} >
{#if item.privacy && item.privacy.visibility === "private"}

View File

@ -76,7 +76,7 @@
</Col>
</Row>
{:else}
<span class="itemcounter">{members.length} {pageOptions.type}s ({currentMembers.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
<span class="itemcounter">{processedMembers.length} {pageOptions.type}s ({currentMembers.length} shown) <a href="#!" on:click={(e) => {e.preventDefault(); fetchList()}}>Refresh list</a></span>
<ListPagination bind:currentPage={pageOptions.currentPage} {pageAmount} />

View File

@ -3,6 +3,7 @@ import type { ListOptions, PageOptions } from './types';
export function filterList(list: Group[]|Member[], groups: Group[], options: ListOptions, type?: string): Group[]|Member[] {
let searchedList = search(list, options);
let groupedList = [...searchedList];
if (type)
groupedList = group(searchedList, groups, options, type);
@ -143,8 +144,7 @@ function sort<T extends Member|Group>(list: T[], options: ListOptions): T[] {
return newList;
}
function group<T extends Member|Group>(members: Member[], groups: Group[], options: ListOptions, type?: string): Group[]|Member[] {
let list = type === "member" ? [...members] : [...groups] || []
function group(list: Member[]|Group[], groups: Group[], options: ListOptions, type?: string): Group[]|Member[] {
let groupFilterList = [...list]
if (options.groups.filter === "include")

View File

@ -131,7 +131,7 @@
}
$: groupMembers = $members.filter(m => group.members.includes(m.uuid));
$: processedList = filterList(groupMembers, listOptions);
$: processedList = filterList(groupMembers, $groups, listOptions);
$: currentPage = paginateList(processedList, pageOptions);
$: pageAmount = getPageAmount(processedList, pageOptions);

View File

@ -134,7 +134,7 @@
}
$: memberGroups = $groups.filter(g => g.members.includes(member.uuid));
$: processedList = filterList(memberGroups, listOptions);
$: processedList = filterList(memberGroups, $groups,listOptions);
$: currentPage = paginateList(processedList, pageOptions);
$: pageAmount = getPageAmount(processedList, pageOptions);
</script>

View File

@ -31,7 +31,7 @@
icon_privacy: "Icon",
list_privacy: "Member list",
metadata_privacy: "Metadata",
visibility: "Visbility",
visibility: "Visibility",
};
async function submit() {

View File

@ -1,14 +0,0 @@
FROM alpine:latest as builder
RUN apk add go
WORKDIR /build
COPY . /build/
RUN go build .
FROM alpine:latest
COPY --from=builder /build/web-proxy /bin/web-proxy
ENTRYPOINT [ "/bin/web-proxy" ]

View File

@ -1,28 +0,0 @@
app = "pluralkit"
kill_signal = "SIGTERM"
kill_timeout = 5
[metrics]
port = 9091
path = "/metrics"
[deploy]
strategy = "bluegreen"
[[services]]
internal_port = 8080
protocol = "tcp"
[services.concurrency]
hard_limit = 500
soft_limit = 400
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443

View File

@ -1,3 +0,0 @@
module web-proxy
go 1.19

View File

@ -1,63 +0,0 @@
package main
import (
"context"
"log"
"net/http"
"net/http/httputil"
"net/url"
"time"
)
func proxyTo(host string) *httputil.ReverseProxy {
rp := httputil.NewSingleHostReverseProxy(&url.URL{
Scheme: "http",
Host: host,
RawQuery: "",
})
rp.ModifyResponse = logTimeElapsed
return rp
}
// todo: this shouldn't be in this repo
var remotes = map[string]*httputil.ReverseProxy{
"api.pluralkit.me": proxyTo("pluralkit-api.flycast:5000"),
"dash.pluralkit.me": proxyTo("pluralkit-compute02._peer.internal:8080"),
"sentry.pluralkit.me": proxyTo("pluralkit-compute02._peer.internal:9000"),
"grafana.pluralkit.me": proxyTo("pluralkit-db1._peer.internal:3000"),
}
type ProxyHandler struct{}
func (p ProxyHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
remote, ok := remotes[r.Host]
if !ok {
// unknown domains redirect to landing page
http.Redirect(rw, r, "https://pluralkit.me", http.StatusFound)
return
}
r.Header.Del("X-PluralKit-Client-IP")
r.Header.Set("X-PluralKit-Client-IP", r.Header.Get("Fly-Client-IP"))
startTime := time.Now()
r = r.WithContext(context.WithValue(r.Context(), "req-time", startTime))
remote.ServeHTTP(rw, r)
}
func logTimeElapsed(resp *http.Response) error {
r := resp.Request
startTime := r.Context().Value("req-time").(time.Time)
elapsed := time.Since(startTime)
log.Printf("[%s] \"%s %s%s\" %d - %vms %s\n", r.Header.Get("Fly-Client-IP"), r.Method, r.Host, r.URL.Path, resp.StatusCode, elapsed.Milliseconds(), r.Header.Get("User-Agent"))
return nil
}
func main() {
http.ListenAndServe(":8080", ProxyHandler{})
}