Compare commits
10 Commits
f112f45e77
...
4d65237ee1
Author | SHA1 | Date |
---|---|---|
Iris System | 4d65237ee1 | |
Iris System | 6b14c50f09 | |
Iris System | a0fa03599b | |
Jake Fulmine | 11d70666d0 | |
Jake Fulmine | 99f7044bab | |
Jake Fulmine | ac91093847 | |
Jake Fulmine | c3ade8f455 | |
Jake Fulmine | b4e7f9a6c3 | |
Jake Fulmine | 96622c5bd4 | |
Jake Fulmine | 434ed3e50c |
|
@ -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)
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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} />
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
icon_privacy: "Icon",
|
||||
list_privacy: "Member list",
|
||||
metadata_privacy: "Metadata",
|
||||
visibility: "Visbility",
|
||||
visibility: "Visibility",
|
||||
};
|
||||
|
||||
async function submit() {
|
||||
|
|
|
@ -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" ]
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
module web-proxy
|
||||
|
||||
go 1.19
|
|
@ -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{})
|
||||
}
|
Loading…
Reference in New Issue