Author SHA1 Message Date
wayfinderak 648bb34237 fix(ci): disable optional GitHub build-record and QEMU cache uploads
Build and Push Container / docker (pull_request) Successful in 3m16s
2026-09-11 10:25:22 +00:00
wayfinderak 4ec10db3bc Merge pull request 'Scope delivery checks and isolate build jobs' (#1) from feat/delivery-streamlining-20260911 into main
Build and Push Container / docker (push) Successful in 10m20s
2026-09-11 09:46:28 +00:00
wayfinderak 5e6f5bd518 Resolve upstream Actions explicitly and check out the exact event revision
Build and Push Container / docker (pull_request) Successful in 10m14s
2026-09-11 09:14:35 +00:00
wayfinderak 9ddab5f1fa feat(delivery): publish immutable source images and declare release
Build and Push Container / docker (pull_request) Failing after 5s
2026-09-11 08:53:23 +00:00
wayfinderak 8502885a9d Cancel superseded pull request validation without cancelling releases 2026-09-11 08:47:07 +00:00
wayfinderak 20c1efb8a3 Scope documentation CI and isolate delivery job resources 2026-09-11 08:10:38 +00:00
wayfinderak 779c73d07f Stop tracking Portainer secrets
Build and Push Container / docker (push) Successful in 18m8s
2026-05-07 18:39:35 -08:00
wayfinderak 4d95fc9de9 Add Portainer deployment files 2026-05-07 13:55:20 -08:00
wayfinderak e9a23fb8ad Migrate deployment to Gitea Actions and Portainer 2026-03-22 11:43:22 -08:00
Dustin Pianalto df8f5f7430 Update registry 2021-10-05 23:47:29 -08:00
Dustin Pianalto c3dfe0d3af Import os 2021-10-02 18:34:18 -08:00
Dustin Pianalto 6bcd17e5e3 Update deployment and action 2021-10-02 18:28:05 -08:00
Dustin Pianalto 52719a1543 Update actions 2021-02-18 23:30:10 -09:00
Dustin Pianalto 5b65158206 Add member join and remove logging 2021-02-18 23:18:20 -09:00
Dustin Pianalto 3282d9bd1f Initial DO workflow test 2021-01-13 01:02:53 -09:00
Dustin Pianalto b99a915fde Add method to move links to link channel 2020-12-12 00:43:23 -09:00
Dustin Pianalto 1156f796ec Add method to move links to link channel 2020-12-12 00:35:27 -09:00
Dustin Pianalto 4ebdf09917 Change name of event handler 2020-11-18 23:05:05 -09:00
Dustin Pianalto d9c5ec623c Add case where there is a / after ref 2020-11-18 21:11:44 -09:00
Dustin Pianalto c64d3ca748 Fix bug related to www 2020-11-14 01:24:28 -09:00
Dustin Pianalto 4088af1ba7 Use user's nick if exists 2020-11-14 01:14:34 -09:00
Dustin Pianalto 321cd9aff2 Add github actions 2020-11-14 00:46:51 -09:00
Dustin Pianalto 903e021f0b Add base structure and url webhook logic 2020-11-14 00:42:37 -09:00
Dusty.P f029184fa6 Initial commit 2020-11-13 15:00:42 -09:00
20 changed files with 601 additions and 93 deletions
+5
View File
@@ -0,0 +1,5 @@
# Service release contract
Butler resolves this repository's protected source revision to verified immutable images. The platform updates only image references in the existing production Compose file and restarts only the already running application services. It preserves current environment, storage, routes, and provider configuration. It retains previous application images and restores them if readiness fails.
Migration and maintenance images are pinned when declared, but this release operation does not run database migrations or maintenance jobs. A release requiring a schema change needs the corresponding explicit platform migration first. No homelab image-pin commit is required for an ordinary compatible application release.
+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"deployment": "prepbot-release",
"environment": "production",
"images": {
"prepbot_release_prepbot_image": {
"name": "prepbot",
"tag": "sha-{revision}"
}
},
"revision_variable": "prepbot_release_revision"
}
+6
View File
@@ -0,0 +1,6 @@
.git
.github
.gitea
.env
.idea
prepbot
+92
View File
@@ -0,0 +1,92 @@
name: Build and Push Container
on:
push:
branches:
- main
tags:
- v*
workflow_dispatch: null
pull_request:
branches:
- main
env:
REGISTRY: gitea.wayfinderak.com
IMAGE_NAME: wayfinderak/prepbot
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number
|| github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
docker:
runs-on: butler-ci
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Select relevant delivery work
id: delivery
env:
DELIVERY_EVENT: ${{ github.event_name }}
DELIVERY_REF: ${{ github.ref }}
DELIVERY_SHA: ${{ github.sha }}
DELIVERY_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
cd .
bash scripts/ci-select.sh
- name: Set up QEMU
if: ${{ steps.delivery.outputs.run == 'true' }}
uses: https://github.com/docker/setup-qemu-action@v3
with:
cache-image: "false"
- name: Set up Docker Buildx
if: ${{ steps.delivery.outputs.run == 'true' }}
uses: https://github.com/docker/setup-buildx-action@v3
- name: Log in to Gitea registry
if: ${{ (steps.delivery.outputs.run == 'true') && github.event_name != 'pull_request' }}
uses: https://github.com/docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Prepare image tags
if: ${{ steps.delivery.outputs.run == 'true' }}
id: prep
shell: bash
run: |
short_sha="${GITHUB_SHA::7}"
tags="${REGISTRY}/${IMAGE_NAME}:sha-${short_sha}"
tags+=$'\n'"${REGISTRY}/${IMAGE_NAME}:sha-${GITHUB_SHA}"
if [[ "${GITHUB_REF_TYPE}" == "branch" && ("${GITHUB_REF_NAME}" == "master" || "${GITHUB_REF_NAME}" == "main") ]]; then
tags+=$'\n'"${REGISTRY}/${IMAGE_NAME}:latest"
fi
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
version="${GITHUB_REF_NAME#v}"
tags+=$'\n'"${REGISTRY}/${IMAGE_NAME}:${version}"
fi
{
echo 'tags<<EOF'
echo "$tags"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Build and push image
if: ${{ steps.delivery.outputs.run == 'true' }}
uses: https://github.com/docker/build-push-action@v6
env:
DOCKER_BUILD_RECORD_UPLOAD: "false"
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.prep.outputs.tags }}
labels: org.opencontainers.image.revision=${{ github.sha }}
+51 -49
View File
@@ -1,62 +1,64 @@
name: CI name: Build and Push Container
# Controls when the action will run. Triggers the workflow on push to master or development
# with a tag like v1.0.0 or v1.0.0-dev
on: on:
push: push:
tags: branches:
- v[0-9]+.[0-9]+.[0-9]+ - master
- v[0-9]+.[0-9]+.[0-9]+-[a-zA-Z]+ - main
tags:
- v*
workflow_dispatch:
env:
REGISTRY: gitea.wayfinderak.com
IMAGE_NAME: wayfinderak/prepbot
jobs: jobs:
build: docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - name: Checkout
- uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Configure AWS credentials - name: Set up QEMU
uses: aws-actions/configure-aws-credentials@v1 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea registry
uses: docker/login-action@v3
with: with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} registry: ${{ env.REGISTRY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} username: ${{ secrets.REGISTRY_USERNAME }}
aws-region: us-east-1 password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Login to Amazon ECR - name: Prepare image tags
id: login-ecr id: prep
uses: aws-actions/amazon-ecr-login@v1 shell: bash
- name: Get Version
id: get_version
uses: battila7/get-version-action@v2.0.0
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: prepbot
IMAGE_TAG: ${{ steps.get_version.outputs.version-without-v }}
run: | run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . short_sha="${GITHUB_SHA::7}"
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG tags="${REGISTRY}/${IMAGE_NAME}:sha-${short_sha}"
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- name: Fill in the new image ID in the Amazon ECS task definition if [[ "${GITHUB_REF_TYPE}" == "branch" && ("${GITHUB_REF_NAME}" == "master" || "${GITHUB_REF_NAME}" == "main") ]]; then
id: task-def tags+=$'\n'"${REGISTRY}/${IMAGE_NAME}:latest"
uses: aws-actions/amazon-ecs-render-task-definition@v1 fi
with:
task-definition: task-definition.json
container-name: "prepbot"
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
uses: aws-actions/amazon-ecs-deploy-task-definition@v1 version="${GITHUB_REF_NAME#v}"
tags+=$'\n'"${REGISTRY}/${IMAGE_NAME}:${version}"
fi
{
echo 'tags<<EOF'
echo "$tags"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Build and push image
uses: docker/build-push-action@v6
with: with:
task-definition: ${{ steps.task-def.outputs.task-definition }} context: .
service: "prepbot" push: true
cluster: "discord-bots" platforms: linux/amd64,linux/arm64
wait-for-service-stability: true tags: ${{ steps.prep.outputs.tags }}
+2
View File
@@ -15,3 +15,5 @@
# vendor/ # vendor/
.env .env
portainer-app.env
+10 -12
View File
@@ -1,20 +1,18 @@
FROM golang:1.15-alpine as dev FROM golang:1.22-alpine AS builder
WORKDIR /go/src/prepbot WORKDIR /src
COPY ./go.mod . RUN apk add --no-cache ca-certificates git tzdata
COPY ./go.sum .
COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN go install github.com/dustinpianalto/prepbot/... ARG TARGETARCH=amd64
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -o /out/prepbot ./cmd/prepbot
CMD [ "go", "run", "cmd/prepbot/main.go"] FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
from alpine COPY --from=builder /out/prepbot /usr/local/bin/prepbot
WORKDIR /bin ENTRYPOINT ["/usr/local/bin/prepbot"]
COPY --from=dev /go/bin/prepbot ./prepbot
CMD [ "prepbot" ]
+21
View File
@@ -0,0 +1,21 @@
# Portainer deployment notes
Generated from the current Kubernetes deployment in namespace `discord-bots`. Secret values are in the local `portainer-*.env` files.
## Kubernetes source
- Deployment: `prepbot`
- Replicas: `1`
- Rolling update: maxSurge `1`, maxUnavailable `1`
- minReadySeconds: `120`
- Labels/selectors: `app=prepbot`
- Image pull secrets: `none required for Portainer stack`
## Containers
- `prepbot`: image `gitea.wayfinderak.com/wayfinderak/prepbot:latest`; env: DISCORD_TOKEN, LINK_CHANNEL, CHAT_CHANNEL; ports: none; requests: {'cpu': '1', 'memory': '512Mi'}; limits: {'cpu': '2', 'memory': '1Gi'}
## Portainer files
- `portainer-stack.yml` - Docker Compose stack to paste/use in Portainer.
- `portainer-app.env` - bot runtime secrets/env.
+9 -4
View File
@@ -6,9 +6,10 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"github.com/bwmarrin/discordgo" "github.com/dustinpianalto/discordgo"
"github.com/dustinpianalto/disgoman" "github.com/dustinpianalto/disgoman"
"github.com/dustinpianalto/prepbot/internal/exts/messages" "gitea.wayfinderak.com/wayfinderak/prepbot/internal/exts/members"
"gitea.wayfinderak.com/wayfinderak/prepbot/internal/exts/messages"
) )
func main() { func main() {
@@ -20,7 +21,9 @@ func main() {
} }
dg.State.MaxMessageCount = 100 dg.State.MaxMessageCount = 100
dg.StateEnabled = true dg.StateEnabled = true
dg.LogLevel = 3
dg.Debug = false
dg.Identify = discordgo.Identify{ dg.Identify = discordgo.Identify{
Intents: discordgo.MakeIntent(discordgo.IntentsAll), Intents: discordgo.MakeIntent(discordgo.IntentsAll),
} }
@@ -39,7 +42,9 @@ func main() {
CheckPermissions: false, CheckPermissions: false,
} }
dg.AddHandler(messages.OnMessage) dg.AddHandler(messages.CleanAmazonURLs)
dg.AddHandler(members.OnGuildMemberAddLogging)
dg.AddHandler(members.OnGuildMemberRemoveLogging)
err = dg.Open() err = dg.Open()
if err != nil { if err != nil {
+50
View File
@@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: prepbot
namespace: discord-bots
labels:
app: prepbot
spec:
replicas: 1
selector:
matchLabels:
app: prepbot
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
minReadySeconds: 120
template:
metadata:
labels:
app: prepbot
spec:
containers:
- name: prepbot
image: <IMAGE>
resources:
requests:
memory: "512Mi"
cpu: "1"
limits:
memory: "1Gi"
cpu: "2"
env:
- name: DISCORD_TOKEN
valueFrom:
secretKeyRef:
name: prepbot
key: discord_token
- name: LINK_CHANNEL
valueFrom:
secretKeyRef:
name: prepbot
key: link_channel
- name: CHAT_CHANNEL
valueFrom:
secretKeyRef:
name: prepbot
key: chat_channel
imagePullSecrets:
- name: registry-1
+9
View File
@@ -0,0 +1,9 @@
services:
prepbot:
image: ${IMAGE:-gitea.wayfinderak.com/wayfinderak/prepbot:${IMAGE_TAG:-latest}}
container_name: prepbot
restart: unless-stopped
environment:
DISCORD_TOKEN: ${DISCORD_TOKEN}
LINK_CHANNEL: ${LINK_CHANNEL}
CHAT_CHANNEL: ${CHAT_CHANNEL}
+4 -3
View File
@@ -1,8 +1,9 @@
module github.com/dustinpianalto/prepbot module gitea.wayfinderak.com/wayfinderak/prepbot
go 1.14 go 1.14
require ( require (
github.com/bwmarrin/discordgo v0.22.0 github.com/bwmarrin/discordgo v0.29.0 // indirect
github.com/dustinpianalto/disgoman v0.0.15 github.com/dustinpianalto/discordgo v0.0.4
github.com/dustinpianalto/disgoman v0.0.26
) )
+16 -9
View File
@@ -1,10 +1,17 @@
github.com/bwmarrin/discordgo v0.20.2/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM= github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= github.com/dustinpianalto/discordgo v0.0.4 h1:KlIkMzDH4jKRbs5IxeqeIXtNTDVcDi/sHV1gaKYEnPg=
github.com/dustinpianalto/disgoman v0.0.15 h1:kdIw6jhC82WBut7+4BarqxBw06dozU+Hu47LQzkkoGM= github.com/dustinpianalto/discordgo v0.0.4/go.mod h1:LKY7UDCPJMChgrowLagHjDSj1gWbt3Hx+6bRFjpaVg8=
github.com/dustinpianalto/disgoman v0.0.15/go.mod h1:v3FM6n+4dH9XlvO+IDx6MN3DUnGq6YVDBvy1A1k202g= github.com/dustinpianalto/disgoman v0.0.26 h1:7p6hDbNgEehUgOHigY/lHDY4P2VklJiZmOn674ka1h0=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/dustinpianalto/disgoman v0.0.26/go.mod h1:WsDb+7XIm2MJ6xhxRAiXlzSPuSYIGSr4rWRmACeoing=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+64
View File
@@ -0,0 +1,64 @@
package discord_utils
import (
"fmt"
"time"
)
func ParseDateString(inTime time.Time) string {
d := time.Now().Sub(inTime)
s := int64(d.Seconds())
days := s / 86400
s = s - (days * 86400)
hours := s / 3600
s = s - (hours * 3600)
minutes := s / 60
seconds := s - (minutes * 60)
dateString := ""
if days != 0 {
dateString += fmt.Sprintf("%v days ", days)
}
if hours != 0 {
dateString += fmt.Sprintf("%v hours ", hours)
}
if minutes != 0 {
dateString += fmt.Sprintf("%v minutes ", minutes)
}
if seconds != 0 {
dateString += fmt.Sprintf("%v seconds ", seconds)
}
if dateString != "" {
dateString += " ago."
} else {
dateString = "Now"
}
stamp := inTime.Format("2006-01-02 15:04:05")
return fmt.Sprintf("%v\n%v", dateString, stamp)
}
func ParseDurationString(inDur time.Duration) string {
s := int64(inDur.Seconds())
days := s / 86400
s = s - (days * 86400)
hours := s / 3600
s = s - (hours * 3600)
minutes := s / 60
seconds := s - (minutes * 60)
durString := ""
if days != 0 {
durString += fmt.Sprintf("%v days ", days)
}
if hours != 0 {
durString += fmt.Sprintf("%v hours ", hours)
}
if minutes != 0 {
durString += fmt.Sprintf("%v minutes ", minutes)
}
if seconds != 0 {
durString += fmt.Sprintf("%v seconds ", seconds)
}
if durString == "" {
durString = "0 seconds"
}
return fmt.Sprintf("%v", durString)
}
+32
View File
@@ -0,0 +1,32 @@
package discord_utils
import "time"
type Snowflake struct {
CreationTime time.Time
WorkerID int8
ProcessID int8
Increment int16
}
func ParseSnowflake(s int64) Snowflake {
const (
DISCORD_EPOCH = 1420070400000
TIME_BITS_LOC = 22
WORKER_ID_LOC = 17
WORKER_ID_MASK = 0x3E0000
PROCESS_ID_LOC = 12
PROCESS_ID_MASK = 0x1F000
INCREMENT_MASK = 0xFFF
)
creationTime := time.Unix(((s>>TIME_BITS_LOC)+DISCORD_EPOCH)/1000.0, 0)
workerID := (s & WORKER_ID_MASK) >> WORKER_ID_LOC
processID := (s & PROCESS_ID_MASK) >> PROCESS_ID_LOC
increment := s & INCREMENT_MASK
return Snowflake{
CreationTime: creationTime,
WorkerID: int8(workerID),
ProcessID: int8(processID),
Increment: int16(increment),
}
}
+123
View File
@@ -0,0 +1,123 @@
package members
import (
"fmt"
"log"
"strconv"
"time"
"github.com/dustinpianalto/discordgo"
"gitea.wayfinderak.com/wayfinderak/prepbot/internal/discord_utils"
)
var joinChannelID = "768571410133418037"
func OnGuildMemberAddLogging(s *discordgo.Session, member *discordgo.GuildMemberAdd) {
defer func() {
if r := recover(); r != nil {
log.Println("Recovered from panic in OnGuildMemberAddLogging", r)
}
}()
guild, err := s.State.Guild(member.GuildID)
if err != nil {
log.Println(err)
return
}
var title string
if member.User.Bot {
title = "Bot Joined"
} else {
title = "Member Joined"
}
thumb := &discordgo.MessageEmbedThumbnail{
URL: member.User.AvatarURL(""),
}
int64ID, _ := strconv.ParseInt(member.User.ID, 10, 64)
snow := discord_utils.ParseSnowflake(int64ID)
field := &discordgo.MessageEmbedField{
Name: "User was created:",
Value: discord_utils.ParseDateString(snow.CreationTime),
Inline: false,
}
embed := &discordgo.MessageEmbed{
Title: title,
Description: fmt.Sprintf("%v (%v) Has Joined the Server", member.User.Mention(), member.User.ID),
Color: 0x0cc56a,
Thumbnail: thumb,
Footer: &discordgo.MessageEmbedFooter{
Text: fmt.Sprintf("Current Member Count: %v", guild.MemberCount),
IconURL: guild.IconURL("1024"),
},
Timestamp: member.JoinedAt.Format(time.RFC3339),
Fields: []*discordgo.MessageEmbedField{field},
}
s.ChannelMessageSendEmbed(joinChannelID, embed)
}
func OnGuildMemberRemoveLogging(s *discordgo.Session, member *discordgo.GuildMemberRemove) {
defer func() {
if r := recover(); r != nil {
log.Println("Recovered from panic in OnGuildMemberAddLogging", r)
}
}()
timeNow := time.Now()
guild, err := s.State.Guild(member.GuildID)
if err != nil {
log.Println(err)
return
}
var title string
if member.User.Bot {
title = "Bot Left"
} else {
title = "Member Left"
}
thumb := &discordgo.MessageEmbedThumbnail{
URL: member.User.AvatarURL(""),
}
desc := ""
al, err := s.GuildAuditLog(member.GuildID, "", "", 20, 1)
if err != nil {
log.Println(err)
} else {
for _, log := range al.AuditLogEntries {
if log.TargetID == member.User.ID {
int64ID, _ := strconv.ParseInt(log.ID, 10, 64)
logSnow := discord_utils.ParseSnowflake(int64ID)
if timeNow.Sub(logSnow.CreationTime).Seconds() <= 10 {
user, err := s.User(log.UserID)
if err == nil {
desc = fmt.Sprintf("%v (%v) was Kicked by: %v\nReason: %v", member.User.String(), member.User.ID, user.String(), log.Reason)
} else {
desc = fmt.Sprintf("%v (%v) was Kicked by: %v\nReason: %v", member.User.String(), member.User.ID, log.UserID, log.Reason)
}
break
}
}
}
}
if desc == "" {
desc = fmt.Sprintf("%v (%v) Has Left the Server", member.User.String(), member.User.ID)
}
embed := &discordgo.MessageEmbed{
Title: title,
Description: desc,
Color: 0xff9431,
Thumbnail: thumb,
Footer: &discordgo.MessageEmbedFooter{
Text: fmt.Sprintf("Current Member Count: %v", guild.MemberCount),
IconURL: guild.IconURL("1024"),
},
Timestamp: timeNow.Format(time.RFC3339),
}
s.ChannelMessageSendEmbed(joinChannelID, embed)
}
+44 -16
View File
@@ -2,16 +2,18 @@ package messages
import ( import (
"log" "log"
"os"
"regexp" "regexp"
"strings" "strings"
neturl "net/url"
"github.com/bwmarrin/discordgo" "github.com/dustinpianalto/discordgo"
) )
func OnMessage(s *discordgo.Session, message *discordgo.MessageCreate) { func CleanAmazonURLs(s *discordgo.Session, message *discordgo.MessageCreate) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
log.Println("Recovered from panic in OnMessage") log.Println("Recovered from panic in CleanAmazonURLs")
} }
}() }()
if message.Author.Bot { if message.Author.Bot {
@@ -21,19 +23,45 @@ func OnMessage(s *discordgo.Session, message *discordgo.MessageCreate) {
amazonRegexString := `(http|https):\/\/((?:[\w-_\.]*)amazon(?:\.[\w\-_]+)+)([\w\/\-\.,@?^=%&amp;~\+#]*[\w\-\@?^=%&amp;/~\+#])?` amazonRegexString := `(http|https):\/\/((?:[\w-_\.]*)amazon(?:\.[\w\-_]+)+)([\w\/\-\.,@?^=%&amp;~\+#]*[\w\-\@?^=%&amp;/~\+#])?`
amazonRegex := regexp.MustCompile(amazonRegexString) amazonRegex := regexp.MustCompile(amazonRegexString)
urls := amazonRegex.FindAllString(message.Content, -1) urls := amazonRegex.FindAllString(message.Content, -1)
if len(urls) == 0 { if len(urls) != 0 {
return
} for _, url := range urls {
for _, url := range urls { u, _ := neturl.Parse(url)
if strings.Contains(url, "ref=") || strings.Contains(url, "?") { u.RawQuery = ""
parts := strings.Split(url, "/") u.Fragment = ""
new := strings.Join(parts[:len(parts)-1], "/") if ( url != u.String() ) {
content = strings.ReplaceAll(content, url, new) content = strings.ReplaceAll(content, url, u.String())
}
}
_, err := sendWebhook(s, message, message.ChannelID, content)
if err == nil {
s.ChannelMessageDelete(message.ChannelID, message.ID)
} }
} }
webhook, err := s.WebhookCreate(message.ChannelID, message.ID, "") message.Content = content
urlRegexString := `http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+`
urlRegex := regexp.MustCompile(urlRegexString)
urls = urlRegex.FindAllString(message.Content, -1)
if len(urls) != 0 {
moveNewsLinks(s, message)
}
}
func moveNewsLinks(s *discordgo.Session, message *discordgo.MessageCreate) {
linkChannel := os.Getenv("LINK_CHANNEL")
chatChannel := os.Getenv("CHAT_CHANNEL")
if message.ChannelID == chatChannel {
_, err := sendWebhook(s, message, linkChannel, message.Content)
if err != nil {
log.Println(err)
}
}
}
func sendWebhook(s *discordgo.Session, message *discordgo.MessageCreate, channelID, content string) (string, error) {
webhook, err := s.WebhookCreate(channelID, message.ID, "")
if err != nil { if err != nil {
return return "", err
} }
defer s.WebhookDelete(webhook.ID) defer s.WebhookDelete(webhook.ID)
var name string var name string
@@ -47,9 +75,9 @@ func OnMessage(s *discordgo.Session, message *discordgo.MessageCreate) {
Username: name, Username: name,
AvatarURL: message.Author.AvatarURL(""), AvatarURL: message.Author.AvatarURL(""),
} }
_, err = s.WebhookExecute(webhook.ID, webhook.Token, true, params) w, err := s.WebhookExecute(webhook.ID, webhook.Token, true, params)
if err != nil { if err != nil {
return return "", err
} }
s.ChannelMessageDelete(message.ChannelID, message.ID) return w.ID, nil
} }
+3
View File
@@ -0,0 +1,3 @@
DISCORD_TOKEN=replace-me
LINK_CHANNEL=replace-me
CHAT_CHANNEL=replace-me
+9
View File
@@ -0,0 +1,9 @@
services:
prepbot:
image: gitea.wayfinderak.com/wayfinderak/prepbot:latest
container_name: prepbot
restart: unless-stopped
env_file:
- ./portainer-app.env
mem_limit: 1g
cpus: "2.0"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Keep required jobs alive; only skip reviewed non-runtime documentation paths.
set -euo pipefail
base="${DELIVERY_BASE:-}"
head="$(git rev-parse HEAD)"
run=true
if [[ "${DELIVERY_REF:-}" != refs/tags/* && "$head" == "${DELIVERY_SHA:-}" && "${DELIVERY_EVENT:-}" =~ ^(push|pull_request)$ && "$base" =~ ^[0-9a-f]{40}$ && "$base" != 0000000000000000000000000000000000000000 ]]; then
if ! git cat-file -e "$base^{commit}" 2>/dev/null; then
if [[ -n "${DELIVERY_FETCH_TOKEN:-}" ]]; then
git -c "http.extraHeader=Authorization: token ${DELIVERY_FETCH_TOKEN}" fetch --quiet --depth=1 origin "$base" >/dev/null 2>&1 || true
else
git fetch --quiet --depth=1 origin "$base" >/dev/null 2>&1 || true
fi
fi
if git cat-file -e "$base^{commit}" 2>/dev/null; then
paths="$(mktemp)"
trap 'rm -f -- "$paths"' EXIT
# Snapshot comparison is conservative when main advanced during a PR.
# Both rename sides remain visible, including names containing newlines.
if git diff --no-renames --name-only -z "$base" "$head" > "$paths"; then
run=false
while IFS= read -r -d '' path; do
case "$path" in
README.md|CHANGELOG.md|LICENSE|LICENSE.md) ;;
*) run=true; break ;;
esac
done < "$paths"
fi
fi
fi
if [[ "$run" == false ]]; then
echo 'Only reviewed non-runtime documentation changed; build and validation are not applicable.'
else
echo 'Runtime, unknown inputs, or unavailable event history: run required validation.'
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "run=$run" >> "$GITHUB_OUTPUT"
fi
printf '%s\n' "$run"