0

Merge remote-tracking branch 'origin/develop' into webv2

This commit is contained in:
Gabe Kangas 2022-07-11 21:21:07 -07:00
commit 506d1fa4cf
No known key found for this signature in database
GPG Key ID: 9A56337728BC81EA
16 changed files with 695 additions and 2043 deletions

View File

@ -1,40 +0,0 @@
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
# for details.
name: Build nightly docker
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *'
jobs:
Docker:
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Earthly version
run: earthly --version
- name: Log into GitHub Container Registry
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
if: env.GH_CR_PAT != null
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: all
- uses: actions/checkout@v3
- name: Checkout and build
if: env.GH_CR_PAT != null
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: cd build/release && ./docker-nightly-earthly.sh

View File

@ -1,24 +1,40 @@
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
# for details.
name: Build nightly docker name: Build nightly docker
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: "0 2 * * *" - cron: '0 2 * * *'
jobs: jobs:
Docker: Docker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: earthly/actions-setup@v1
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Earthly version
run: earthly --version
- name: Log into GitHub Container Registry - name: Log into GitHub Container Registry
env: env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }} GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
if: env.GH_CR_PAT != null if: env.GH_CR_PAT != null
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: all
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Setup and run - name: Checkout and build
env: if: env.GH_CR_PAT != null
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }} GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: cd build/release && ./docker-nightly.sh run: cd build/release && ./docker-nightly.sh
if: env.GH_CR_PAT != null

View File

@ -178,11 +178,11 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi
} }
if err != nil { if err != nil {
err = errors.Wrap(err, "error resolving publickey from iri") err = errors.Wrap(err, "error resolving publickey from iri, actor may not be valid: "+publicKeyIRI)
} }
if !resolved { if !resolved {
err = errors.New("error resolving publickey from iri") err = errors.New("error resolving publickey from iri, actor may not be valid: " + publicKeyIRI)
} }
return pubkey, err return pubkey, err

View File

@ -1,14 +0,0 @@
#!/bin/sh
# Docker build
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
DOCKER_IMAGE="owncast-earthly"
DATE=$(date +"%Y%m%d")
VERSION="${DATE}-nightly"
echo "Building Docker image ${DOCKER_IMAGE}..."
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"

View File

@ -1,29 +1,14 @@
#!/bin/sh
# Docker build # Docker build
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages # Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
DOCKER_IMAGE="owncast" DOCKER_IMAGE="owncast"
DATE=$(date +"%Y%m%d") DATE=$(date +"%Y%m%d")
VERSION="${DATE}-nightly" VERSION="${DATE}-nightly"
GIT_COMMIT=$(git rev-list -1 HEAD)
# Create production build of Tailwind CSS
pushd ../../build/javascript >> /dev/null
# Install the tailwind & postcss CLIs
npm install --quiet --no-progress
# Run the tailwind CLI and pipe it to postcss for minification.
# Save it to a temp directory that we will reference below.
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "../../webroot/js/web_modules/tailwindcss/dist/tailwind.min.css"
popd
echo "Building Docker image ${DOCKER_IMAGE}..." echo "Building Docker image ${DOCKER_IMAGE}..."
# Change to the root directory of the repository # Change to the root directory of the repository
cd $(git rev-parse --show-toplevel) cd $(git rev-parse --show-toplevel)
# Docker build earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t ghcr.io/owncast/${DOCKER_IMAGE}:nightly .
# Dockerhub
# You must be authenticated via `docker login` with your Dockerhub credentials first.
# docker push gabekangas/owncast:nightly
docker push ghcr.io/owncast/${DOCKER_IMAGE}:nightly

View File

@ -686,7 +686,7 @@ paths:
type: string type: string
example: https://fediverse.biz/authorize_interaction?uri=https://my.owncast.server/federation/user/streamer example: https://fediverse.biz/authorize_interaction?uri=https://my.owncast.server/federation/user/streamer
/api/chat/updatemessagevisibility: /api/chat/messagevisibility:
post: post:
summary: Update the visibility of chat messages. summary: Update the visibility of chat messages.
description: Pass an array of IDs you want to change the chat visibility of. description: Pass an array of IDs you want to change the chat visibility of.
@ -951,7 +951,7 @@ paths:
type: string type: string
format: date-time format: date-time
/api/admin/chat/updatemessagevisibility: /api/admin/chat/messagevisibility:
post: post:
summary: Update the visibility of chat messages. summary: Update the visibility of chat messages.
description: Pass an array of IDs you want to change the chat visibility of. description: Pass an array of IDs you want to change the chat visibility of.
@ -1984,7 +1984,7 @@ paths:
type: string type: string
format: date-time format: date-time
/api/integrations/chat/updatemessagevisibility: /api/integrations/chat/messagevisibility:
post: post:
summary: Update the visibility of chat messages. summary: Update the visibility of chat messages.
description: Pass an array of IDs you want to change the chat visibility of. description: Pass an array of IDs you want to change the chat visibility of.

View File

@ -131,7 +131,7 @@ func Start() error {
http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages)) http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages))
// Update chat message visibility // Update chat message visibility
http.HandleFunc("/api/admin/chat/updatemessagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility)) http.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
// Enable/disable a user // Enable/disable a user
http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled)) http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled))
@ -318,7 +318,7 @@ func Start() error {
// Inline chat moderation actions // Inline chat moderation actions
// Update chat message visibility // Update chat message visibility
http.HandleFunc("/api/chat/updatemessagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility)) http.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
// Enable/disable a user // Enable/disable a user
http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled)) http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled))

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

View File

@ -7,112 +7,112 @@ const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage; const sendChatMessage = require('./lib/chat').sendChatMessage;
const testVisibilityMessage = { const testVisibilityMessage = {
body: 'message ' + Math.floor(Math.random() * 100), body: 'message ' + Math.floor(Math.random() * 100),
type: 'CHAT', type: 'CHAT',
}; };
var messageId; var messageId;
const establishedUserFailedChatMessage = { const establishedUserFailedChatMessage = {
body: 'this message should fail to send ' + Math.floor(Math.random() * 100), body: 'this message should fail to send ' + Math.floor(Math.random() * 100),
type: 'CHAT', type: 'CHAT',
}; };
test('can send a chat message', async (done) => { test('can send a chat message', async (done) => {
const registration = await registerChat(); const registration = await registerChat();
const accessToken = registration.accessToken; const accessToken = registration.accessToken;
sendChatMessage(testVisibilityMessage, accessToken, done); sendChatMessage(testVisibilityMessage, accessToken, done);
}); });
test('verify we can make API call to mark message as hidden', async (done) => { test('verify we can make API call to mark message as hidden', async (done) => {
const registration = await registerChat(); const registration = await registerChat();
const accessToken = registration.accessToken; const accessToken = registration.accessToken;
const ws = new WebSocket( const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`, `ws://localhost:8080/ws?accessToken=${accessToken}`,
{ {
origin: 'http://localhost:8080', origin: 'http://localhost:8080',
} }
); );
// Verify the visibility change comes through the websocket // Verify the visibility change comes through the websocket
ws.on('message', async function incoming(message) { ws.on('message', async function incoming(message) {
const messages = message.split('\n'); const messages = message.split('\n');
messages.forEach(async function (message) { messages.forEach(async function (message) {
const event = JSON.parse(message); const event = JSON.parse(message);
if (event.type === 'VISIBILITY-UPDATE') { if (event.type === 'VISIBILITY-UPDATE') {
ws.close(); ws.close();
done(); done();
} }
}); });
}); });
const res = await request const res = await request
.get('/api/admin/chat/messages') .get('/api/admin/chat/messages')
.auth('admin', 'abc123') .auth('admin', 'abc123')
.expect(200); .expect(200);
const message = res.body[0]; const message = res.body[0];
messageId = message.id; messageId = message.id;
await request await request
.post('/api/admin/chat/updatemessagevisibility') .post('/api/admin/chat/messagevisibility')
.auth('admin', 'abc123') .auth('admin', 'abc123')
.send({ idArray: [messageId], visible: false }) .send({ idArray: [messageId], visible: false })
.expect(200); .expect(200);
}); });
test('verify message has become hidden', async (done) => { test('verify message has become hidden', async (done) => {
await new Promise((r) => setTimeout(r, 2000)); await new Promise((r) => setTimeout(r, 2000));
const res = await request const res = await request
.get('/api/admin/chat/messages') .get('/api/admin/chat/messages')
.expect(200) .expect(200)
.auth('admin', 'abc123'); .auth('admin', 'abc123');
const message = res.body.filter((obj) => { const message = res.body.filter((obj) => {
return obj.id === messageId; return obj.id === messageId;
}); });
expect(message.length).toBe(1); expect(message.length).toBe(1);
// expect(message[0].hiddenAt).toBeTruthy(); // expect(message[0].hiddenAt).toBeTruthy();
done(); done();
}); });
test('can enable established chat user mode', async (done) => { test('can enable established chat user mode', async (done) => {
await request await request
.post('/api/admin/config/chat/establishedusermode') .post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123') .auth('admin', 'abc123')
.send({ value: true }) .send({ value: true })
.expect(200); .expect(200);
done(); done();
}); });
test('can send a message after established user mode is enabled', async (done) => { test('can send a message after established user mode is enabled', async (done) => {
const registration = await registerChat(); const registration = await registerChat();
const accessToken = registration.accessToken; const accessToken = registration.accessToken;
sendChatMessage(establishedUserFailedChatMessage, accessToken, done); sendChatMessage(establishedUserFailedChatMessage, accessToken, done);
}); });
test('verify rejected message is not in the chat feed', async (done) => { test('verify rejected message is not in the chat feed', async (done) => {
const res = await request const res = await request
.get('/api/admin/chat/messages') .get('/api/admin/chat/messages')
.expect(200) .expect(200)
.auth('admin', 'abc123'); .auth('admin', 'abc123');
const message = res.body.filter((obj) => { const message = res.body.filter((obj) => {
return obj.body === establishedUserFailedChatMessage.body; return obj.body === establishedUserFailedChatMessage.body;
}); });
expect(message.length).toBe(0); expect(message.length).toBe(0);
done(); done();
}); });
test('can disable established chat user mode', async (done) => { test('can disable established chat user mode', async (done) => {
await request await request
.post('/api/admin/config/chat/establishedusermode') .post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123') .auth('admin', 'abc123')
.send({ value: false }) .send({ value: false })
.expect(200); .expect(200);
done(); done();
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1188,9 +1188,9 @@
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
}, },
"moment": { "moment": {
"version": "2.29.2", "version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
}, },
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",

View File

@ -7,7 +7,7 @@ export const URL_CONFIG = `/api/config`;
export const URL_VIEWER_PING = `/api/ping`; export const URL_VIEWER_PING = `/api/ping`;
// inline moderation actions // inline moderation actions
export const URL_HIDE_MESSAGE = `/api/chat/updatemessagevisibility`; export const URL_HIDE_MESSAGE = `/api/chat/messagevisibility`;
export const URL_BAN_USER = `/api/chat/users/setenabled`; export const URL_BAN_USER = `/api/chat/users/setenabled`;
// TODO: This directory is customizable in the config. So we should expose this via the config API. // TODO: This directory is customizable in the config. So we should expose this via the config API.

View File

@ -3,132 +3,133 @@ import htm from '/js/web_modules/htm.js';
import { URL_FOLLOWERS } from '/js/utils/constants.js'; import { URL_FOLLOWERS } from '/js/utils/constants.js';
const html = htm.bind(h); const html = htm.bind(h);
export default class FollowerList extends Component { export default class FollowerList extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
followers: [], followers: [],
followersPage: 0, followersPage: 0,
currentPage: 0, currentPage: 0,
total: 0, total: 0,
}; };
} }
componentDidMount() { componentDidMount() {
try { try {
this.getFollowers(); this.getFollowers();
} catch (e) { } catch (e) {
console.error('followers error: ', e); console.error('followers error: ', e);
} }
} }
async getFollowers() { async getFollowers(requestedPage) {
const { currentPage } = this.state; const limit = 24;
const limit = 24; const offset = requestedPage * limit;
const offset = currentPage * limit; const u = `${URL_FOLLOWERS}?offset=${offset || 0}&limit=${limit}`;
const u = `${URL_FOLLOWERS}?offset=${offset}&limit=${limit}`; const response = await fetch(u);
const response = await fetch(u); const followers = await response.json();
const followers = await response.json(); const pages = Math.ceil(followers.total / limit);
this.setState({ this.setState({
followers: followers.results, followers: followers.results,
total: response.total, total: followers.total,
}); pages: pages,
} });
}
changeFollowersPage(page) { changeFollowersPage(requestedPage) {
this.setState({ currentPage: page }); this.setState({ currentPage: requestedPage });
this.getFollowers(); this.getFollowers(requestedPage);
} }
render() { render() {
const { followers, total, currentPage } = this.state; const { followers, total, pages, currentPage } = this.state;
if (!followers) { if (!followers) {
return null; return null;
} }
const noFollowersInfo = html`<div class="col-span-4"> const noFollowersInfo = html`<div class="col-span-4">
<p class="mb-5 text-2xl">Be the first to follow this live stream.</p> <p class="mb-5 text-2xl">Be the first to follow this live stream.</p>
<p class="text-md"> <p class="text-md">
By following this stream you'll get updates when it goes live, receive By following this stream you'll get updates when it goes live, receive
posts from the streamer, and be featured here as a follower. posts from the streamer, and be featured here as a follower.
</p> </p>
<p class="text-md mt-5"> <p class="text-md mt-5">
Learn more about ${' '} Learn more about ${' '}
<a class="underline" href="https://en.wikipedia.org/wiki/Fediverse" <a class="underline" href="https://en.wikipedia.org/wiki/Fediverse"
>The Fediverse</a >The Fediverse</a
>, where you can follow this server as well as so much more. >, where you can follow this server as well as so much more.
</p> </p>
</div>`; </div>`;
const paginationControls = const paginationControls =
total > 1 && pages > 1 &&
Array(total) Array(pages)
.fill() .fill()
.map((x, n) => { .map((x, n) => {
const activePageClass = const activePageClass =
n === currentPage && n === currentPage &&
'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white'; 'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white';
return html` <li class="page-item active w-10"> return html` <li class="page-item active w-10">
<a <a
class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}" class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}"
onClick=${() => this.changeFollowersPage(n)} onClick=${() => this.changeFollowersPage(n)}
> >
${n + 1} ${n + 1}
</a> </a>
</li>`; </li>`;
}); });
return html` return html`
<div> <div>
<div <div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
> >
${followers.length === 0 && noFollowersInfo} ${followers.length === 0 && noFollowersInfo}
${followers.map((follower) => { ${followers.map((follower) => {
return html` <${SingleFollower} user=${follower} /> `; return html` <${SingleFollower} user=${follower} /> `;
})} })}
</div> </div>
<div class="flex"> <div class="flex">
<nav aria-label="Tab pages"> <nav aria-label="Tab pages">
<ul class="flex list-style-none flex-wrap"> <ul class="flex list-style-none flex-wrap">
${paginationControls} ${paginationControls}
</ul> </ul>
</nav> </nav>
</div> </div>
</div> </div>
`; `;
} }
} }
function SingleFollower(props) { function SingleFollower(props) {
const { user } = props; const { user } = props;
const { name, username, link, image } = user; const { name, username, link, image } = user;
var displayName = name; var displayName = name;
var displayUsername = username; var displayUsername = username;
if (!displayName) { if (!displayName) {
displayName = displayUsername.split('@', 1)[0]; displayName = displayUsername.split('@', 1)[0];
} }
return html` return html`
<a <a
href=${link} href=${link}
class="following-list-follower block bg-white flex p-2 rounded-xl shadow border hover:no-underline mb-3 mr-3" class="following-list-follower block bg-white flex p-2 rounded-xl shadow border hover:no-underline mb-3 mr-3"
target="_blank" target="_blank"
> >
<img <img
src="${image || '/img/logo.svg'}" src="${image || '/img/logo.svg'}"
class="w-16 h-16 rounded-full" class="w-16 h-16 rounded-full"
onError=${({ currentTarget }) => { onError=${({ currentTarget }) => {
currentTarget.onerror = null; currentTarget.onerror = null;
currentTarget.src = '/img/logo.svg'; currentTarget.src = '/img/logo.svg';
}} }}
/> />
<div class="p-3 truncate flex-grow"> <div class="p-3 truncate flex-grow">
<p class="font-semibold text-gray-700 truncate">${displayName}</p> <p class="font-semibold text-gray-700 truncate">${displayName}</p>
<p class="text-sm text-gray-500 truncate">${displayUsername}</p> <p class="text-sm text-gray-500 truncate">${displayUsername}</p>
</div> </div>
</a> </a>
`; `;
} }